activerecord-importと:on_duplicate_key_ignore
オプションを組み合わせるとカラム定義の範囲外の値であっても無理やりINSERTすることができます。
# frozen_string_literal: true require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "activerecord", "6.1.0" gem "activerecord-import" gem "mysql2" end require "active_record" require "logger" ActiveRecord::Base.establish_connection(adapter: "mysql2", database: "test", username: "root") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do create_table :users, force: true do |t| t.string :name, index: { unique: true } t.decimal :money, precision: 10 end end class User < ActiveRecord::Base end attributes = [ { name: "foo", money: "10000000000" }, { name: "foo", money: "20000000000" }, ] User.import(attributes, on_duplicate_key_ignore: true) # User.insert_all(attributes) puts puts User.pluck(:money) # => 9999999999 puts
% ruby foo.rb Fetching gem metadata from https://rubygems.org/.............. Fetching gem metadata from https://rubygems.org/. Resolving dependencies... Using concurrent-ruby 1.1.7 Using bundler 2.2.3 Using minitest 5.14.3 Using zeitwerk 2.4.2 Using mysql2 0.5.3 Using i18n 1.8.7 Using tzinfo 2.0.4 Using activesupport 6.1.0 Using activemodel 6.1.0 Using activerecord 6.1.0 Using activerecord-import 1.0.7 -- create_table(:users, {:force=>true}) D, [2021-01-06T14:04:10.296375 #81282] DEBUG -- : (66.5ms) DROP TABLE IF EXISTS `users` D, [2021-01-06T14:04:10.377990 #81282] DEBUG -- : (80.7ms) CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` varchar(255), `money` decimal(10), UNIQUE INDEX `index_users_on_name` (`name`)) -> 0.1967s D, [2021-01-06T14:04:10.397652 #81282] DEBUG -- : ActiveRecord::InternalMetadata Load (0.5ms) SELECT `ar_internal_metadata`.* FROM `ar_internal_metadata` WHERE `ar_internal_metadata`.`key` = 'environment' LIMIT 1 D, [2021-01-06T14:04:10.417055 #81282] DEBUG -- : (0.3ms) SELECT @@max_allowed_packet D, [2021-01-06T14:04:10.424171 #81282] DEBUG -- : User Create Many (6.8ms) INSERT IGNORE INTO `users` (`name`,`money`) VALUES ('foo',10000000000),('foo',20000000000) D, [2021-01-06T14:04:10.428391 #81282] DEBUG -- : (3.4ms) SELECT `users`.`money` FROM `users` 9999999999
https://gist.github.com/kamipo/3db82c3bb7cbcbf007b4d4367a5c5227
これはどういう原理かというと、activerecord-importではINSERTしたいけどすでに(ユニークキーが)おなじレコードがあるときはスルーしたい(i.e. on_duplicate_key_ignore)という機能を実現するのにMySQLではINSERT IGNORE構文を使っていて、INSERT IGNOREではINSERT中のすべてのエラーを無視して無効な値は可能ならもっとも近い値に調整してINSERTするという振る舞いをするため、このような挙動を引き起こすことができます。
では、INSERTしたいけどすでに(ユニークキーが)おなじレコードがあるときはスルーしたい、けど無効な値はちゃんとエラーにしてほしいときはどうしたらいいかというと、Rails 6.0から使えるinsert_all
というバルクインサート用のAPIを使うことができます。RailsチームではわいがMySQLチョットデキルので、この問題についてはレビューでフィードバックして対処されており安心してご利用になることができます。
See also
それでは本年も引き続きよろしくおねがいいたします。