ハトネコエ Web がくしゅうちょう

プログラミングやサーバー関連、チームマネジメントなど得た技術のまとめ

Rails 4 から Rails 5 へのアップデートでおこなった10個以上の対応

関わっている会社の Rails アプリを 4 から 5 に上げましたので、おこなったことを書いておきます。

1. なぜ上げたかったか

Rails 5 は多くの便利メソッドが足されていて開発がいっそう効率的になる、だとか、
世の中で書かれている記事の多くが Rails 5 を対象にしているものになってきた、だとか、
Rails 6 が安定してきて、Rails 4 の対応を外す gem がそろそろ増えていきそう、などの理由があります。

そして一番大きかった理由が Ruby 2.6 にした場合に deprecation warning が出てしまったこと、ここにありました。

/bundle/gems/activesupport-4.2.11.3/lib/active_support/core_ext/object/duplicable.rb:111: warning: BigDecimal.new is deprecated; use BigDecimal() method instead.

実は Rails 4 は v4.2.11.3 が 2020/05/15 にリリースされるなど、
今でもしっかりセキュリティメンテナンスがおこなわれています。

しかし Ruby 2.5 については来年2021年3月にはサポート終了することが
Ruby のリリースサイクル上決まっています。

deprecation warning を無視して Ruby 2.6 に上げる手段も取れますが、
この警告が出ているということは Ruby 2.7 ではメソッドが削除されることを示唆されており、*1
そのような警告を無視したまま Ruby のバージョンを上げてしまうのは危険です。

deprecation warning が確認できない状態に周辺を揃えてから Ruby のバージョンは上げるべきです。

……というわけで、セキュリティサポートの対象から外れないよう
Rails 4 を2021年3月までに卒業しなくては、という意識でいました。
(最悪警告を無視して Ruby 2.6 に上げるとしても、それのサポート終了は 2022年3月)

2. 下準備

Rails 5 に上げることは頭の中にあったので、着々と準備を進めていました。

関わり始めた2019年12月の時点では、

でしたので、リリースノートの確認とステージングでの入念な確認をおこないつつ、

  • Ruby 2.4.9 へのバージョンアップ
  • Rails 4.2.11.1 へのバージョンアップ
  • Ruby 2.5.7 へのバージョンアップ
  • Rails 4.2.11.3 へのバージョンアップ

を順々におこなっていきました。

Ruby のバージョンアップや Rails のパッチバージョンアップはわりと破壊的変更が少ないので、期待していたとおり特に新たな対応をおこなう必要なくバージョンアップをおこなえました。
Ruby 2.5 系で CSV.generate のバグがあるのでそこだけ注意ですかね。

また、不必要な gem の削除や、gem のバージョンアップを可能な限りおこない、
依存関係上 Rails 5 に上げる妨げとなる refile からの脱却もするなど(単純に refile で出来ないことがあったのが一番の理由だけど)、
着々と準備を進めてきました。

おかげで、 Gemfile 内を

gem 'rails', '5.0.0'

と書き換えた上で bundle update rails をおこなってもエラーが起こらない状態にまではなっていました。

テストについては、gem の機能そのものを試しているような無駄なテストがある一方で、
モデルやビューなどのテストがほぼ皆無でしたので、機会を見つけて少しずつ(ほんの少しですが)足していました。

3. Rails 5 に上げた際の作業内容

主にこちらの記事を参考にしました。

3-1. Rails 5.2.4.3 を選択

最初は Rails 5.0.0 にアップデートして少しずつ上げていく予定でしたが、
v5.0.0 では bin/rails app:update をおこなうと

undefined method `instance' for ActiveSupport::Deprecation:Class (NoMethodError)

とエラーが出てしまい、これを解決するには v5.0.6 を使うという話を見て、
「そういう Rails 5 の初期の方のバグを踏むくらいなら、はじめから Rails 5 の最新版を使ったほうがいいのでは」という気持ちになってきて、
5系の最新である v5.2.4.3 にアップデートすることに決めました。

……ただし、これはサービス利用者があまり多くなく、ふだんのDB書き込み量から考えて、
ある程度の問題が起きても対応できると考えての判断なので、
通常は Rails 5.0 → 5.1 → 5.2 と上げていくことをおすすめします。

Rails 5.0 で deprecated warning、Rails 5.1 でメソッド削除、などの変更が、
特に Rails 5.0 と 5.1 間では多いですから、慎重を取るに越したことはありません。

Gemfile に

gem 'rails', '5.2.4.3'

と記載したら

bundle update rails

です。

3-2. bin/rails app:update

bin/rails app:update

で config ファイルの更新などがおこなえます。

上書きするか聞かれる時に「d」を押せば diff が見られますので、
それを見て上書きするか考えると良いでしょう。

diff を見ることで、「あれ? もしかしてこの config ってなくなってる?」などに気付けます。
例えば config.raise_in_transactional_callbacks なんかは Rails 5.0 で deprecated、Rails 5.1 で削除されています。

3-3. まずは web サーバーが起動するようになるまで

私の場合は routing に問題があって引っかかりました。

resources :user_feedbacks, only: :index do
  get "/:year/:month", on: :collection, to: :monthly_index, as: :monthly
end

と書かれている場所が「controller 指定しないとダメよ〜」ってエラーが出ていたので、

resources :user_feedbacks, only: :index do
  get "/:year/:month", on: :collection, to: "user_feedbacks#monthly_index", as: :monthly
end

と直しました。

Web サーバーが起動しない系の問題は、明確なエラーが出るのでここはなんとかなることでしょう。

3-4. Single Table Inheritance

STI (Single Table Inheritance) の仕組みを使っている箇所があったのですが、これが通常の Rails way に乗っかっていない書き方であったため少し苦労しました。 親のモデルに self. find_sti_class(type) メソッドと self. sti_name メソッドをいい感じに定義してあげることでなんとかしました。

どんな感じかと言うと、Music モデルから派生する Jazz モデル、Rock モデルがあるとして、
通常は Music.find(123).is_a?(Jazz) みたく判定させればいいのですが、
Music.find(123).jazz? で判定させたかったようで、

class Music < ActiveRecord::Base
  enum type: { jazz: "Jazz", rock: "Rock" }
end

このように type カラムについての定義がありました。
しかしこれだと、 jazz モデルは存在しないので
SubclassNotFound エラーを吐いてしまいます。

そういうわけで、 find_sti_class, sti_name メソッドを上書きするように

class Music < ActiveRecord::Base
  enum type: { jazz: "Jazz", rock: "Rock" }

  class << self
    def find_sti_class(type)
      type.camelize.constantize
    end

    def sti_name
      types[name.demodulize.underscore]
    end
  end
end

と書いたわけです。本来のコードとは離れるので、動くけど微妙感もあります。

今思いついたんですけど、enum 的な使用が jazz?, rock? メソッドだけなのであれば、

以下のような対処でよかったですね。
不必要に override しちゃうのは今後問題を起こす可能性があるので、これでもいけるか後で見ておきます。

class Music < ActiveRecord::Base
  def jazz?
    is_a?(Jazz)
  end

  def rock?
    is_a?(Rock)
  end
end

余談ですが、Single Table Inheritance のような機能は、
サービス運営を続けていくうちに共通部分がほとんどなくなる、というケースが往々にしてあるので、できれば使わないほうがいいと思います。

3-5. belongs_to にデフォルトでバリデーションが働くようになったことへの対応

Rails 5 への変更で一番大きかったのはここだと思います。

class Song < ActiveRecord::Base
  belongs_to :vocal
end

のような定義がある場合、Song 作成時・更新時に vocal_id が nil だと
バリデーションエラーが出るようになりました。

vocal_id が nil の場合がありえるのであれば、

class Song < ActiveRecord::Base
  belongs_to :vocal, optional: true
end

と書かなくてはいけません。optional: true を付ける必要があります。

逆に言えば、今まで

class Song < ActiveRecord::Base
  belongs_to :vocal
  validates :vocal_id, presence: true
end

となっていたのであれば、 validates の行を消しても問題ないわけですね。

この対応はしっかりおこなわないと、
アプリケーションエラーが起こらないぶん、ユーザーが困っているのに開発者が気付かない、ということが起こりえますので充分に確認しましょう。
自信がなければすべてに optional: true を付けてしまうのも、無くはない選択だと思います。

3-6. skip_before_filter を skip_before_action に

xxxxx_filter コールバックは無くなり xxxxx_action に統一されましたので、 skip_before_filterskip_before_action に、 before_filterbefore_action に変更しましょう。

3-7. create.js → create.js.erb に

Rails非同期通信処理をおこないたい場合に、
create.js というファイル名でも ERB 記法を埋め込むことが出来ましたが、
.erb の拡張子でないとおこなえないように変更されていますので修正しました。

3-8. Ridgepole に --allow-pk-change オプションを付ける

Rails 5 では、id カラムのデフォルトの型が integer から bigint に変更されました。

id の型を bigint にしたい場合は、ridgepole の実行に --allow-pk-change オプションを付与することをお勧めします。
このオプションを使用するには、ridgepole が v0.7.1 以上である必要があります。

オプションを付けていないと rspec 実行時にこんな warning を吐いたりします。

[WARNING] Primary key definition of `versions` differ but `allow_pk_change` option is false
  from: {:id=&gt;:serial}
    to: {}

もしくは、id の型を integer のままにする選択もあります。
テーブル作成のオプションとして MySQL の場合は id: :integerPostgreSQL の場合は id: :serial を追加します。
参考: https://kamihikouki.hatenablog.com/entry/2017/04/11/141447

私は integer のままにする選択を取りました。
vocal_id などの外部キーと関連するカラムも bigint に変更する必要がありますし、
bigint は integer の2倍の 8 Byte ですから、多少データベースサイズが大きくなってしまうことが予想されます。

integer で足りないということは21億行を超えるデータ数となるわけですが、
将来的にそうなりそうなテーブルはほとんどなかったため、 bigint に変えない選択を取りました。
必要に応じて bigint に変更していけばいいと思います。

3-9. autoload 廃止への対策

Rails 5 では autoload が development 環境以外では効かなくなりました

この変更はけっこう悪魔的で、
development 環境では autoload が効くので、
本番(ステージング)にリリースした時に初めて動かないことに気付く、ということが起こりえます。

詳しくはこちらをお読みください ↓
https://qiita.com/joooee0000/items/3ab0f3d791e0d0beb639

いくつかの解決方法が述べられていますが、Rails 4 からのバージョンアップの場合であれば、

config.enable_dependency_loading = true

config/application.rb に足して autoload を本番環境でも有効にする、というやり方が一番安全だと思います。
autoload を廃止しようという動きからこの変更がなされたことを考えると邪道に思いますが、
開発環境で動くのに本番環境で動かない、という恐ろしい罠を避けるには安全を取りたいかなと……。

autoload_paths に追加しているのが lib だけならば、

config.paths.add "lib", eager_load: true

と書くやり方でいいと思います。

3-10. time 型の挙動変更への対応

Rails 5.1 から time 型はデフォルトでタイムゾーンに対応しました。
参考: https://blog.willnet.in/entry/2015/06/12/063731

これにより、データベースのタイムゾーンUTC、アプリケーションのタイムゾーンが日本時間の場合、
Rails 4 では日本時間 19:00 を示す場合、データベースにも 19:00 と保存されていましたが、
Rails 5.1 ではデータベースに 10:00 と保存されるようになります。

つまり!

Rails 4 の時代にデータベースに 19:00 と保存されているデータが、
Rails 5.1 では日本時間 04:00 と解釈されます! 時刻がズレます!!

これを防ぐためには、 config/application.rb

config.active_record.time_zone_aware_types = [:datetime]

と記述しましょう。

私はリリース後にこの問題に気付いてしまったので、 config を変えるのではなく
保存されてるデータを 9 時間前にして保存し直すスクリプトを書いて対応しましたが、
ユーザーから見える時間がズレることが1秒たりとも許されない場合は、
上で書いた config を上書きする対応をするしかないかと思います。

3-11. その他いろいろ

autoprefixer に「display: box; じゃなくて display: flex; を使うようにして」って警告出されたので対応したり、

validates :office_email, format: { with: VALID_EMAIL_REGEXP }, if: "office_email.present?"

のように validates の if 内で String を使う方法は廃止されたので、

validates :office_email, format: { with: VALID_EMAIL_REGEXP }, if: Proc.new { |vocal| vocal.office_email.present? }

というふうに直したり、 enum の扱いの微妙な違いなど、
細かい修正をいくらかおこないました。

モデルについての仕様変更が多いように感じました。
主要なモデルのテストを作っておいたために気付けたことが多く、テストの偉大さを再認識しました。

3-12. あとでやればいいこと

  • ActiveRecord::Base を継承した ApplicationRecord を作成し、model はそれを継承するようにする
  • form_for, form_tag から form_with への切り替え
  • jquery_ujs を rails-ujs に変える

これらも Rails 5 においての大きな変化ですが、急いでおこなう必要はないです
Rails 5 対応は大きなプルリクエストになりますから、必要以上に diff を増やしてレビュアーの負担を増やす必要もないと思います。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

ApplicationRecord クラスを作ってそれを継承するようにすることは、
文字列置換をすればササッとできることなのでやってもいいと思いますが、
Rails 5 対応リリース後の別のプルリクで良いでしょう。

form_with に切り替えるのは、失敗した場合の影響が大きいので、
remote: true がデフォルトになっていることなどの理解を深めつつ、
Rails 5 対応リリース後に少しずつ進めていきましょう。

jquery_ujs を rails-ujs に切り替えるのも注意が必要です。
例えば SweetAlert2 のような Rails の操作に関係する JavaScript ライブラリを使っている場合、実装方法を大きく変える必要があります

rails-ujs は jQuery 依存をなくすために新たに1から書き直されてる別のライブラリだという認識を持ち、
現在 jQuery への依存があるなら「別に変えなくてもいいか」という気持ちも大切です。

フロントエンドにおけるミスは、開発者が気付かずにユーザーが困って初めて気付く、ということが起こりがちですので。

4. Rails 5 対応をリリースしての感想

ステージングでの1週間程度の様子見のあと、本番リリースしました。

作業前にRailsガイドで触れてる変更点なんかを眺める限りだと、
「関係ありそうな箇所はそんなに多くないかな」と思ってしまうんですけれど、
実際に作業してみるとけっこう対応しないといけない点ありましたね……。

3-10. で書いたように、time 型の挙動の違いについてはリリース後に気付くという失敗をしでかしたのであせりましたが、
データベースに影響する問題はそれくらいでしたのでよかったです。
リリースしてから2日程度はそれらバグ対応をおこないましたが、その後は安定しています。

特にリリースしてサイト速度が上がったなどの実感はありませんが、
Rails 5 に上げられたことで Rails 6 へのバージョンアップの道もだいぶ見えてきました。
今年12月にリリースされる予定の Ruby 3.0 にも対応できるよう、着々と準備を進めていきたいですね!

*1:実際エラーになるようです https://tech.recruit-mp.co.jp/server-side/post-19932/

AWS S3 内の画像をすべてリサイズする方法( goofys 使用)

S3 にある大量の画像をリサイズしたい

ページ内に表示する画像の容量が大きすぎると、ページ表示に時間がかかってユーザー体験を損ねるばかりか、
AWS の通信料金も多くかかってしまい、サービス提供側の金銭にも直接ダメージを与えます。

以前 shrine への移行について書いたように、
Rails などの Web アプリケーションでは、
がんばってサムネを作り直してコードを書き換えて……で、(めちゃくちゃ大変だけど)なんとかなります。

一方で、Wordpress のような CMS では、
記事内のみならずウィジェットやユーザーアイコンなど、様々な目的のためにアップロードされた画像を
すべて一括でリサイズするというのは、なかなか簡単なことではありません。

そこで、今回は S3 にある画像自体を書き換えることとしました。

処理に失敗した場合を考え、必ず事前にバックアップを用意しておきましょう。
aws s3 sync コマンドを活用し別バケットにバックアップを取っておくのがおすすめです。( https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html )

1. EC2 インスタンスの用意

今回は、

の EC2 インスタンスを用意し、作業しました。

S3 へのアクセスが必要なので、 AmazonS3FullAccess の権限を持つ IAM Role を作成し、
EC2 インスタンスにアタッチ
しています。

IAM Role の作成についてよくわからない方は、これなどを参考にするといいでしょう: https://www.ritolab.com/entry/16

また、セキュリティグループは自身のIPアドレスからのSSHログインのみ許すよう、しっかり最低限の設定だけしましょう。
それ以外に必要なインバウンドのポートはありません。

2. ImageMagick, pngquant のインストール

EC2インスタンスを作成し、SSHログインしましたら、
今回画像圧縮で用いる ImageMagickpngquant をインストールします。

ImageMagick のインストールはとても簡単です。

sudo yum install -y ImageMagick ImageMagick-devel

次に pngquant のインストールです。

sudo yum install -y gcc git
git clone --recursive https://github.com/kornelski/pngquant.git
cd pngquant
sudo make install

コンパイルのために gcc が必要なのでインストールしています。
特に configure の設定することなく make install すれば、
通常は /usr/local/bin/pngquant に pngquant がインストールされるかと思います。

3. goofys のダウンロード

S3バケットを mount することができる すごいアプリケーション goofys を使います。
同様のツール s3fs をよりパフォーマンス向上させたもののようです。

goofys は go 言語で作られているだけあって、バイナリも配布されています。
それを活用しましょう。

wget https://github.com/kahing/goofys/releases/latest/download/goofys
chmod +x goofys

これで現在のディレクトリに実行可能な goofys が用意されました。
お好みで /usr/local/bin 下への移動などおこなってもよいでしょう。

4. S3 バケットのマウント

goofys を使って S3 バケットをマウントする前に、マウントするためのディレクトリを作成します。
今回は /s3-data という名前にしました。

sudo mkdir /s3-data

必要なパッケージのインストールをします。

sudo yum install -y fuse mailcap

fuse がないと、 goofys でマウントしようとした際に
main.FATAL Unable to mount file system, see syslog for details
とエラーを吐きます。

エラーにある通り syslog を
sudo less /var/log/messages で確認すると
main.FATAL Mounting file system: Mount: mount: running fusermount: exec: "fusermount": executable file not found in $PATH#012#012stderr:
というエラーメッセージが見つかります。fuse がないために fusermount の実行ができないわけですね。

mailcap をインストールする理由は少し変わっていて、mailcap 自体が必要というよりも、
インストールによって /etc/mime.types を作成するのが目的です。

goofys が MIME TYPE を設定するための --use-content-type というオプションがあるのですが、
その際に MIME TYPE を推定するために必要なのが /etc/mime.types なのです。

こうして準備が整ったらマウントをします。
マウントする S3 のバケット名は test-s3-bucket としました。
ここは S3 ですでに作っている実際のバケット名に合わせて変更してください。

sudo ./goofys --use-content-type --acl public-read test-s3-bucket /s3-data

特にメッセージが出ることがなければ成功です
sudo ls -alh /s3-data で、S3 バケットディレクトリが見られるようになっているか確認しましょう。

URL直打ちで画像ファイルを見られるようにしたいので --acl public-read のオプションを付けているところにも注目です。

ちなみに goofys の実行時に sudo を付け忘れると
main.FATAL Mounting file system: Mount: mount: running fusermount: exit status 1#012#012stderr:#012fusermount: user has no write access to mountpoint /s3-data
というエラーが syslog に出力されつつ、マウントに失敗します。
root user でなければ権限が足りないわけですね。

5. 画像圧縮をおこなう

一度 root ユーザーになっておいた方がいろいろおこないやすいです。

sudo su
cd /s3-data

今回は JPEG, PNG それぞれの画像圧縮のために、以下のようなコマンドを打つことにしました。

convertimagemagick のコマンド)のオプションについては、
この間書いた記事をご参照ください。

find /s3-data -type f -name "*.jpg" | xargs -I {} convert {} -colorspace sRGB -resize 1280x1280\> -quality 80 -density 72 {}
find /s3-data -type f -name "*.jpeg" | xargs -I {} convert {} -colorspace sRGB -resize 1280x1280\> -quality 80 -density 72 {}
find /s3-data -type f -name "*.png" | xargs -I {} /usr/local/bin/pngquant --quality 65-80 --force --output {} -- {}
find /s3-data -type f -name "*.png" | xargs -I {} convert {} -resize 1280x1280\> {}

mogrify コマンドはサブディレクトリを掘っていって一括変換、ということが出来ないので、
find コマンドでファイルを探索したのちに xargs で
convert コマンドもしくは pngquant コマンドに渡してあげているのがポイントです。

なお、pngquant の処理後では mime type が goofys デフォルトの application/octet-stream に変わってしまいます。
今回の場合は、その後に imagemagick の処理をやったあとで mime type が image/png に戻ってくれるので気にしていませんが、
pngquant の処理のみおこなう方は、この点ご注意ください。(そしてどうやったら解決できたか教えてください…)

上のコマンド例ではバケット全体にかけていますが、
時間のかかる処理なので、ある程度ディレクトリごとに区切って実施するのが良いかと思います。

6. CDN のキャッシュリセット

CloudFront を使っている場合は、キャッシュをリセットしておきましょう。

Invalidations からおこなえます。(参考: https://dev.classmethod.jp/articles/aws-amazon-cloudfront-deleting-cache-by-invalidation/

おわりに

最初は AWS lambda でやろうとしていたんですが、
慣れない lambda の設定をしてるうちに「1度しかやらない時間のかかる処理を lambda でやらせるのって正しいのか……?」と気になってきたときに、
このStackOverflowの回答を見つけ、「こんなツールあるの! 便利じゃん!」と大きく舵を切り直して、結果的に速く作業を終えることが出来ました。

テスト用のバケットを作って検証を重ねていたので、失敗することなく済み、よかったです。

おまけ:Amazon Linux 2 への libvips のインストール

今回、最初は imagemagick でなく処理がより軽量で高速と聞いている libvips を使おうと思っていたのですが、
リサイズした画像ファイルを上書きする方法がどうしてもわからず結局 imagemagick を使いました。

というわけで、結局使っていないんですが libvips のインストール手順をメモっていたので
ここに残しておきます。

# libvips のインストール
# 参考: 
#   https://github.com/libvips/libvips/issues/1184#issuecomment-447973135
#   https://qiita.com/t-kigi/items/f6850abaaee1db2df5a4

sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install -y http://rpms.remirepo.net/enterprise/remi-release-7.rpm
sudo yum install -y yum-utils
sudo yum-config-manager --enable remi
sudo rpm -ivh http://mirror.centos.org/centos/7/os/x86_64/Packages/LibRaw-0.19.4-1.el7.x86_64.rpm
sudo yum install -y vips vips-devel vips-tools

Next.js に sequelize 導入(メモ)

手順をあとで振り返れるように自分用メモ。

1. clone

を起点とする。リポジトリを clone したのち npm i

2. create database

Docker のこと考えると面倒なので、まずはローカルの MySQL を使うようにするところから。
mysql -u root -p で入った後に

create database next_js_mysql;

3. npm i sequelize mysql2

npm i sequelize mysql2

Ref: https://sequelize.org/master/manual/getting-started.html

4. npm i -D sequelize-cli

Ref: https://sequelize.org/master/manual/migrations.html

npm i -D sequelize-cli
npx sequelize-cli init

config/config.json , models/index.js が作成される。

5. index.js の TS 化

いちおう元の index.js を尊重した上で TypeScript 化するとこんな感じ?

import fs from 'fs';
import path from 'path';
import Sequelize from 'sequelize';

const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
let db = {};

let sequelize: Sequelize.Sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize.Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize.Sequelize(config.database, config.username, config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter(file => {
    return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
  })
  .forEach(file => {
    const model = sequelize['import'](path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

const sequelizeHash = {
  sequelize: sequelize,
  Sequelize: Sequelize,
}

db = { ...db, ...sequelizeHash }

module.exports = db;

6. config.json の更新

development の password と database を更新。

後でわかることだが、 operatorsAliases オプションは deprecated らしいので削除。

7. Article.ts の追加

Ref:

冗長かもしれないが models/Article.ts をこう記述した。

import { Sequelize, Model, DataTypes } from 'sequelize';

export default class Article extends Model {
  public id!: number;
  public title!: string;
  public body!: string;

  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  public static initialize(sequelize: Sequelize) {
    this.init(
      {
        id: {
          type: DataTypes.BIGINT.UNSIGNED,
          primaryKey: true,
          autoIncrement: true,
          allowNull: false,
        },
        title: {
          type: DataTypes.STRING(100),
          allowNull: false,
          defaultValue: '',
        },
        body: {
          type: DataTypes.TEXT,
          allowNull: false,
          defaultValue: '',
        }
      }, {
        tableName: 'article',
        sequelize: sequelize,
      }
    );
    return this;
  }
}

8. index.ts の更新

https://karuta-kayituka.hatenablog.com/entry/2019/07/21/132814 を参考にしながら以下のように更新。

import Sequelize from 'sequelize';
import Article from './Article';

const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];

let sequelize: Sequelize.Sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize.Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize.Sequelize(config.database, config.username, config.password, config);
}

const db = {
  Article: Article.initialize(sequelize),
};

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

export default db;

9. migration file の作成

npx sequelize-cli model:generate --name Article --attributes title:string,body:text で最初作ってみたが、
思っていたのと違うものが出来てしまったので、
migrations/20200502224547-create-article.js は大きく変えて以下のようにした。
collate を指定してるところが地味に大事)

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('article', {
      id: {
        type: Sequelize.BIGINT.UNSIGNED,
        primaryKey: true,
        allowNull: false,
        autoIncrement: true,
      },
      title: {
        type: Sequelize.STRING(100),
        allowNull: false,
        defaultValue: '',
      },
      body: {
        type: Sequelize.TEXT,
        allowNull: false,
        defaultValue: '',
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE,
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE,
      }
    }, {
      charset: 'utf8mb4',
      collate: 'utf8mb4_bin',
    });
  },
  down: (queryInterface) => {
    return queryInterface.dropTable('article');
  }
};

migrate は以下のコマンドで実行。

npx sequelize-cli db:migrate

dry-run できなくてどんなテーブルが作られるか実行後までわからないのは、なかなかひどい仕様だと思う。
migrate だけ ridgepole なり sqldef なり使うのはアリな気がする。

10. API の作成

テーブルが用意できたのであとは API の実装。

pages/api/article/list.ts に以下のように書いた。

import { NextApiRequest, NextApiResponse } from 'next'
import models from '../../../models';

export default (_: NextApiRequest, res: NextApiResponse) => {
  const listArticles = async () => {
    return models.Article.findAll();
  }

  listArticles()
    .then(articles => res.status(200).json({ articles: articles }))
    .catch(() => res.status(500).json({ error: '500: Exception caught' }));
}

pages/api/article/create.ts は以下。

import { NextApiRequest, NextApiResponse } from 'next'
import models from '../../../models';

export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST' && req.headers["content-type"] == 'application/json') {
    const request = req.body;

    const createArticle = async () => {
      return await models.Article.create({
        title: request.title,
        body: request.body,
      });
    }

    if (!request.title || !request.body) {
      return res.status(400).json({ error: '400: Missing parameter' });
    }

    if (request.title.length > 100) {
      return res.status(400).json({ error: '400: title length must be lower than 100 chars' });
    }

    createArticle()
      .then(article => res.status(200).json({ article: article }))
      .catch(() => res.status(500).json({ error: '500: Exception caught' }));
  }
}

create したあとの id が取得できないのが課題。

これで

curl -X POST -H 'Content-Type: application/json' http://localhost:8013/api/article/create -d '{"title": "タイトル", "body": "これが本文です"}'

のような形で作成ができ、

curl http://localhost:8013/api/article/list

で一覧取得できる。

> curl -X POST -H 'Content-Type: application/json' http://localhost:8013/api/article/create -d '{"title": "タイトル", "body": "これが本文です"}'
{"article":{"id":null,"title":"タイトル","body":"これが本文です","updatedAt":"2020-05-03T01:21:34.264Z","createdAt":"2020-05-03T01:21:34.264Z"}}
> curl -X POST -H 'Content-Type: application/json' http://localhost:8013/api/article/create -d '{"title": "タイトル2", "body": "これが本文です2"}'
{"article":{"id":null,"title":"タイトル2","body":"これが本文です2","updatedAt":"2020-05-03T01:21:38.361Z","createdAt":"2020-05-03T01:21:38.361Z"}}
> curl http://localhost:8013/api/article/list
{"articles":[{"id":1,"title":"タイトル","body":"これが本文です","createdAt":"2020-05-03T01:21:34.000Z","updatedAt":"2020-05-03T01:21:34.000Z"},{"id":2,"title":"タイトル2","body":"これが本文です2","createdAt":"2020-05-03T01:21:38.000Z","updatedAt":"2020-05-03T01:21:38.000Z"}]}

11. 感想

db migrate 周りがきつい。
Rails の Schemafile と違ってけっこう文字数多くて打ちにくいし、
migration file と Article.ts で似たようなことを書かないといけないところがなかなかにきつい。

sequelize を使うよりも、db migrate は ridgepole なり sqldef を使うとして、
ふつうにSQL書いたほうが悩むこと少ないのでは、って気分がしてきた。

mogrify コマンドで画像を一括変換しよう

imagemagick というと、
Rails 書いてる人にとっては minimagick (or rmagick) を使うために必要なやつでしょ?
ってイメージのほうが強いと思うけれど、もちろん CLI でも力を発揮してくれる。

1. くすんでしまったレナさん

f:id:nekonenene:20200426152720j:plain
いつもの画像解析でおなじみレナさん……と思いきや?

この画像、ウェブで見ると一見ふつうに見えるのだけれど、
なんとダウンロードして Mac や一部の画像編集ソフトで見ると変色する。

f:id:nekonenene:20200426153857p:plain
変色してしまったレナさん

さらにこのダウンロードした画像を画像編集ソフト「Affinity Photo」png 画像として保存すると、
以下のようにさらに変色する。

f:id:nekonenene:20200426154217p:plain
白く霞んでしまったレナさん

これと同じ現象が関わってる会社でも起こって、
「なんでだろう? でも色味が CMYK っぽいよな」と思って確認したところ、
まさにその通りで、Web向けの画像なのに Color model (色空間の指定)が CMYK になってしまっていた

f:id:nekonenene:20200426154733p:plain
Color model: CMYK (色空間がCMYK

つまり、ウェブブラウザは色空間が CMYK という情報を読み取れないので RGB として描画するのだが、
Mac や高性能な画像編集ソフトの場合は CMYK を理解できるために、CMYK として描画してしまい、
くすんだ色合いになってしまったわけだ。

さらに png で保存すると、png は RGB 形式での保存しか出来ないので、
画像編集ソフトがいい感じに CMYK → RGB の変換をおこなおうとして、結果として白くくすんでしまっていた。

おそらくこの最初のような画像が生まれた背景は、デザイナーさんが書き出し設定を印刷用設定にしたままJPEG出力をしたのだろう。
このような、ウェブ用なのに CMYK 指定が入っているという歪(いびつ)な画像を、今回大量に修正する必要があった。

2. convert コマンド

imagemagick をすでにインストールしてあるとして(Mac なら brew install imagemagick)、
convert コマンドで1枚ごとの変換が可能だ。

convert lenna.jpg -colorspace RGB lenna.jpg

変換前の画像 lenna.jpg の色空間を RGB に変更して上書きできる。

出力後のファイル名を変える場合には

convert lenna.jpg -colorspace RGB lenna_converted.jpg

などとおこなう。

3. mogrify コマンド

上書きの場合であれば、 convert コマンドを使わずに以下のように mogrify コマンドでも可能だ。

mogrify -colorspace RGB lenna.jpg

そしてこのコマンドではワイルドカードを扱えるので、
大量の画像を一括処理したい場合に有効活用できる。

# 注意:以下のコマンドを実行するとディレクトリ内のすべての .jpg 画像に影響が出るのでご注意ください
mogrify -colorspace RGB *.jpg

これで、問題の画像全てを1つのディレクトリにまとめて置いた上で、
そのディレクトリ内でこのコマンドを実行すれば、すべての .jpg 画像の色空間指定が修正される。

4. mogrify コマンドで画像ファイルを一括サイズ縮小

上記のような特殊なケースに限らず、 mogrify コマンドは活用できる。

例えば、「写真素材をWebサイトで使う前に、ユーザーがページ表示に時間かからないよう全部縮小しちゃいたいな」という場合。

以下のコマンドで、
アスペクト比を維持しつつ縦・横のどちらかの最大を 1200px、JPEG品質は 90% にして、72dpi で保存
ということが出来てしまう。

今回は上書きにならないよう、 resized ディレクトリを先に作って、
そこに変換後の画像を出力することとした。

mkdir resized
mogrify -resize 1200x1200\> -quality 90 -density 72 -path resized *.jpg

なお、PNG 画像も同様におこなえる。(ただし JPEG と異なり、quality のパラメーターが小さいほど容量が小さくなるわけではない点に注意)

mogrify -resize 1200x1200\> -quality 90 -density 72 -path resized *.png

他にも出来る画像処理はあるので、公式ドキュメントでオプション一覧を見ると良い。
https://imagemagick.org/script/mogrify.php

Windows だと画像リサイズソフトいっぱいあるのに
Mac だとつらいな〜、と思っていた人にはぜひ試してもらいたい。