Rails の seed.rb に冪等性を持たせる

rake db:seed を何回やっても大丈夫なようにするには auto increment の管理が必要になります。
本番環境で何度も実行することは、ほぼあり得ないし、開発環境であれば直接DBをわさわさしちゃえば間に合うので、まともに実装する必要はないんでしょうが、少し調べてみました。

自前で拡張

sql - Rails way to reset seed on id field - Stack Overflow
reset_pk_sequence というメソッドを追加していますが、面白いのが PostgreSQL の場合 reset_pk_sequence! というメソッドを呼ぶだけのようで、Rails自体に実装されています。
しかし、MySQLは考慮さえされておらず(MySQL用に拡張するのは簡単だと述べられていますが)DB格差が生まれています。

PostgreSQL実装

# active_record/connection_adapters/postgresql/schema_statements.rb
        def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
          (略)

            select_value(<<-end_sql, 'SCHEMA')
              SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
            end_sql

MySQLも考慮してみる

config/initializers 以下に格納。
こんな感じでしょうか。

module ActiveRecord
  class Base
    def self.reset_pk_sequence
      case ActiveRecord::Base.connection.adapter_name
      when "Mysql2"
        new_max = maximum(primary_key) || 0
        update_seq_sql = "alter table #{table_name} auto_increment = 1;"
        ActiveRecord::Base.connection.execute(update_seq_sql)
      when "SQLite"
        new_max = maximum(primary_key) || 0
        update_seq_sql = "update sqlite_sequence set seq = #{new_max} where name = '#{table_name}';"
        ActiveRecord::Base.connection.execute(update_seq_sql)
      when "PostgreSQL"
        ActiveRecord::Base.connection.reset_pk_sequence!(table_name)
      else
        raise "Task not implemented for this DB adapter"
      end
    end
  end
end

database_cleaner

ついでに test でお世話になっている DatabaseCleaner はどういう実装かというと、基本Truncateで処理しSQLiteの場合は sqlite_sequence を delete していました。

# database_cleaner-1.6.1/lib/database_cleaner/active_record/truncation.rb
    module SQLiteAdapter
      def delete_table(table_name)
        execute("DELETE FROM #{quote_table_name(table_name)};")
        if uses_sequence
          execute("DELETE FROM sqlite_sequence where name = '#{table_name}';")
        end
      end
      alias truncate_table delete_table