環境: Rails 6.0、PostgreSQL 12.2、MySQL 5.7
ActiveRecord::ValueTooLong
テーブルにレコードを保存するとき、255文字のように長さに制限があるカラムに300文字の文字列を指定して保存しようとするとどうなるでしょう。
こういうテーブルがあって(MySQL)、
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
| created_at | datetime(6) | NO | | NULL | |
| updated_at | datetime(6) | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
バリデーションを記述せずに300文字保存しようとすると、
User.create!(name: "a" * 300)
例外 ActiveRecord::ValueTooLong
が発生します。
ActiveRecord::ValueTooLong (Mysql2::Error: Data too long for column 'name' at row 1)
PostgreSQLの場合は、マイグレーションのt.string
がデフォルトで制限なしの character varying
になるので、サイズ制限を付けて試します。
def change
create_table :users do |t|
t.string :name, limit: 255
t.timestamps
end
end
300文字保存しようとすると、やはり例外 ActiveRecord::ValueTooLong
が発生します。
ActiveRecord::ValueTooLong (PG::StringDataRightTruncation: ERROR: value too long for type character varying(255))
MySQLの厳密なSQLモード
これを試してみたのは、「長い文字列は自動的に切り落とされるんじゃなかったけ?」という記憶がなんとなくあったからです。MySQLのマニュアルを見てみると……
MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.4.1 CHAR および VARCHAR 型
MySQL :: MySQL 5.7 Reference Manual :: 11.3.2 The CHAR and VARCHAR Types (5.7のマニュアルの日本語版はない)
厳密な SQL モードが有効でない場合に、CHAR または VARCHAR カラムにその最大長を超える値を割り当てると、その値はカラムの最大長に合わせて切り捨てられ、警告メッセージが表示されます。スペース以外の文字の切り捨てに関しては、厳密な SQL モードを使用することで、警告ではなくエラーを発生させて、その値の挿入を抑制できます。
とあります。昔のMySQLには「厳密なSQLモード」(strict SQL mode)がなかったせいで切り落とされていたのかな。厳密なSQLモードのオンオフを切り替えるには……
- MySQL :: MySQL 5.6 リファレンスマニュアル :: 5.1.7 サーバー SQL モード
- MySQL :: MySQL 5.7 Reference Manual :: 5.1.10 Server SQL Modes(5.7のマニュアルの日本語版はない)
厳密な SQL モードは、STRICT_ALL_TABLES または STRICT_TRANS_TABLES のいずれかが有効な場合に有効になりますが、これらのモードの影響はいくらか異なります。
MySQL 5.7のデフォルトSQLモードからSTRICT_TRANS_TABLESを外したものをmy.cnfに設定して試します。
sql-mode="ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
やっぱりActiveRecord::ValueTooLong
が出ます。が、Railsのログで次の出力に気付きました。Railsは自動的に厳密なSQLモードを設定しているらしい。
SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
Railsでこれをオフにするには、database.ymlで strict: false
を設定します。(Rails Guides には見当たらない。参照: https://stackoverflow.com/questions/21420122/how-to-turn-off-mysql-strict-mode-in-rails/21894108 )
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password: xxxxxxxx
socket: /tmp/mysql.sock
strict: false
Railsによって厳密モードが外され、長い文字列が切り落とされるようになりました。my.cnf関係なかったですね。
irb(main):001:0> User.create!(name: "a" * 300)
(11.2ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(REPLACE(REPLACE(REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', ''), 'STRICT_ALL_TABLES', ''), 'TRADITIONAL', ''), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
(0.2ms) BEGIN
User Create (0.3ms) INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '2020-04-11 09:01:39.414519', '2020-04-11 09:01:39.414519')
(11.6ms) COMMIT
=> #<User id: 1, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", created_at: "2020-04-11 09:01:39", updated_at: "2020-04-11 09:01:39">
末尾の空白は切り落とされる
PostgreSQLのマニュアルを見ていたところ、次の記述に気付きました。
PostgreSQL 11.5文書 第8章 データ型 8.3. 文字型
ただし、上限を超えた部分にある文字がすべて空白の場合はエラーにはならず、文字列の最大長にまで切り詰められます。 (この一風変わった例外は標準SQLで要求されています。)
そんな仕様があるとは。試してみたところ、MySQL(厳密モード)でもPostgreSQLでも空白が切り落とされました。
irb(main):002:0> User.create!(name: "a" * 150 + " " * 150)
(0.4ms) BEGIN
User Create (0.4ms) INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ', '2020-04-11 11:00:30.125770', '2020-04-11 11:00:30.125770')
(5.8ms) COMMIT
=> #<User id: 1, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", created_at: "2020-04-11 11:00:30", updated_at: "2020-04-11 11:00:30">
irb(main):003:0> User.last.name.size
User Load (0.5ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
=> 255
以上です。