こんにちは、キエンです。
このエントリはアクトインディアドベントカレンダー2019の6日目です。 adventar.org
さっそく、Railsのマイグレーションで一つ問題を気づいています。Railsのバグか、それとも僕がどこか勘違いしていますが、一旦共有させていただきます。どなたか疑問があれば、教えていただけば嬉しいです。
問題
先日、既存のTIMESTAMP型のカラムをマイグレーションでただコメント属性を追記しようと思いましたが、エラーが発生されました。
- 既存テーブル作成マイグレーション
class CreateArticles < ActiveRecord::Migration[6.0] def change create_table :articles do |t| t.string :name t.timestamp :published_at t.timestamps end end end
published_at
カラムにコメントを追記するマイグレーション
class ChangePublishedAtCommentOnArticles < ActiveRecord::Migration[6.0] def change change_column :articles, :published_at, :timestamp, comment: 'comment' end end
- コメントを追記するマイグレーションを実行するとエラーが発生される
$ bundle exec rake db:migrate Mysql2::Error: Invalid default value for 'published_at'
調査
環境バージョン
Rails | MySQL |
---|---|
6.0.1 | 5.7 |
※Rails 5.xも同じ現象を確認しました。
スキーマとカラム変更SQLの確認
既存テーブルのスキーマ
mysql> SHOW CREATE TABLE articles\G *************************** 1. row *************************** Table: articles Create Table: CREATE TABLE `articles` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `published_at` timestamp NULL DEFAULT NULL, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec)
スキーマを見ると、現在published_at
はTIMESTAMP型にNULL 値を許可していますね。
ここで軽くMySQLのTIMESTAMP型のNULL制約とデフォルト値を説明します。デフォルトでは、TIMESTAMP型はNOT NULLであり、NULL値を含めることはできません。 TIMESTAMP型にNULL値を許可するため、明示的にNULL属性を追加する必要です。
CREATE TABLE t1 ( ts1 TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- default 0 ts2 TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP -- default NULL );
RailsのマイグレーションでNULLのoptionがTRUE、また指定しない場合、NULL値が許可されています。TIMESTAMP型の場合、NULL値を許可するため、Rails側にNULL属性を追加してくれると思っています。
# timestamp型の場合 t.timestamp :published_at ↓ `published_at` timestamp NULL DEFAULT NULL
カラム変更のSQL
change_column :articles, :published_at, :timestamp, comment: 'comment' ↓ ALTER TABLE `articles` CHANGE `published_at` `published_at` timestamp DEFAULT NULL COMMENT 'comment'
published_at
の変更SQLがおかしいですね。timestamp DEFAULT NULL
の原因でMysql2::Error: Invalid default value for 'published_at'
エラーが出ていると思います。
NULL属性がないので、published_at
のデフォルトの値は0のに、DEFAULT NULL
で定義するとinvalid
エラーが発生されているからです。
正確なSQLはこちらだと思います。
- ALTER TABLE `articles` CHANGE `published_at` `published_at` timestamp DEFAULT NULL COMMENT 'comment' + ALTER TABLE `articles` CHANGE `published_at` `published_at` timestamp NULL DEFAULT NULL COMMENT 'comment'
結論、Railsのスキーマ生成処理に何か問題があるでしょう。次、Railsのスキーマ生成処理を確認しましょう。
Railsのスキーマ生成処理確認
schema_creation.rb
# TIMESTAMP NULLになるかこちらの処理で決める def add_column_options!(sql, options) # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP # column to contain NULL, explicitly declare it with the NULL attribute. # See https://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] sql << " NULL" unless options[:null] == false || options_include_default?(options) end end
schema_statements.rb
def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end
上記のschema_creation.rbとschema_statements.rbの処理を見ると、NULL属性が付かれるため、マイグレーションで以下のすべて条件を満たす必要です。
- TIMESTAMP型
- プライマリーキーではない
- NULL: TRUE
- DEFAULTを指定しない
今回コメントを追記するマイグレーションは上記の条件をすべて満たしましたが、なんでNULL属性が付かないですね。
abstract_mysql_adapter.rb
カラム変更の場合、スキーマ生成前に、以下のoptions調整ってメッソドが実行されています。
def change_column_for_alter(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) type ||= column.sql_type unless options.key?(:default) options[:default] = column.default end unless options.key?(:null) options[:null] = column.null end unless options.key?(:comment) options[:comment] = column.comment end td = create_table_definition(table_name) cd = td.new_column_definition(column.name, type, options) schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end
マイグレーションでDEFAULTを指定しない場合既存のカラムのDEFAULTが設定されます。なので、DEFAULTを指定しない
条件を満たさなくになってしまいます。なるほど、動きが理解できました。
ちなみに、マイグレーションでTIMESTAMP型にDEFAULT: NULLを指定してもNULL属性が付かられないです。この条件で問題があると思います。
DEFAULTを指定しない
の条件からDEFAULTを指定しないまたはNULLを指定
に変更すれば方が良いかと考えておきます。
# schema_creation.rb sql << " NULL" unless options[:null] == false || options_include_default?(options) ↓ sql << " NULL" unless options[:null] == false || options[:default].present?
Railsのソースを上書きする
TIMESTAMP型は2038年の問題も含めて、TIMESTAMP型を使用するのはよろしくなくて、DATETIME型にした方が良いと思いますが、絶対にTIMESTAMP型のまましないとだめ、かつこの記事の問題を解決したければ、以下の方針があるだと考えておきます。
MySQLで timestamp型 を使うのはNG!2038年問題の対処法 | PisukeCode - Web開発まとめ
class_evalを使用方法
class_evalを使用して、add_column_optionsメソッドを上書きします。
ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation.class_eval do private def add_column_options!(sql, options) # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP # column to contain NULL, explicitly declare it with the NULL attribute. # See https://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] sql << " NULL" unless options[:null] == false || options[:default].present? end ... end end
DockerイメージでRailsのソースを修正
Dockerファイルに以下の行を追加すれば良いと思います。
RUN sed -i '34s/options_include_default?(options)/options[:default].present?/' \ /usr/local/bundle/gems/activerecord-6.0.1/lib/active_record/connection_adapters/mysql/schema_creation.rb
結果
Railsのソースを上書きしてから、無事にマイグレーションでカラムを変更できました。
# マイグレーションで生成したSQL ALTER TABLE `articles` CHANGE `published_at` `published_at` timestamp NULL DEFAULT NULL COMMENT 'comment' # コメントが追加できました。 mysql> SHOW CREATE TABLE articles\G *************************** 1. row *************************** Table: articles Create Table: CREATE TABLE `articles` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `published_at` timestamp NULL DEFAULT NULL COMMENT 'comment', `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec)
最後に
いかがでしょうか。Railsのソースを読んだら、詳しく処理の仕組みを理解できれば良いですね。 さて、アクトインディではエンジニアを募集していますね、ぜひご応募してください。 actindi.net