部署Rails应用时MySQL报重复列名错误,如何解决?
刚碰到这个报错的时候我也头大,先给你理清楚问题出在哪,再给几个靠谱的解决办法:
首先看报错:
ActiveRecord::StatementInvalid: Mysql2::Error: Duplicate column name 'first_name': ALTER TABLE `users` ADD `first_name` varchar(255)
你的场景我很熟悉:本地环境怎么测都没问题——不管是rails db:reset、删库重建手动迁移,还是导种子数据,schema和查询结果都显示字段正常,但ShipIt部署时偏偏卡在20180507080705_break_up_fullname_in_users.rb迁移的up方法第3行。
先贴一下你的相关代码方便大家看:
拆分名字的迁移代码:
class BreakUpFullnameInUsers < ActiveRecord::Migration[5.2] def up add_column :users, :first_name, :string add_column :users, :last_name, :string User.all.each do |user| fn, ln = name.split(' ', 2) user.update(first_name: fn, last_name: ln) end remove_column :users, :name end def down add_column :users, :name, :string User.all.each do |user| n = [name.first_name, name.last_name].join(' ') user.update(name: n) end remove_column :users, :first_name remove_column :users, :last_name end end
原始建表迁移:
class CreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :email t.string :name t.string :username t.string :password_digest t.timestamps end end end
当前schema的用户表结构:
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "email" t.string "username" t.string "password_digest" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "first_name" t.string "last_name" t.boolean "admin", default: false end
问题根源
这个错误的核心原因很明确:生产环境的数据库里first_name字段已经存在了,但ShipIt在部署时还是尝试执行这个迁移的up方法,触发了MySQL的重复列错误。
为什么会这样?大概率是下面两种情况:
- 之前已经手动执行过这个迁移,或者有人直接修改过生产库的表结构,但
schema_migrations表里没有记录这个迁移版本; - 部署过程中出过中断,导致迁移执行了一半(比如已经加了
first_name,但没完成后续步骤),现在重新部署又从头执行。
解决办法
1. 先查生产环境的迁移状态
首先登录到生产服务器,跑这个命令看看哪些迁移已经执行:
rails db:migrate:status
如果20180507080705_break_up_fullname_in_users.rb已经显示为up,那说明这个迁移早就跑过了,只需要把它的版本号加到schema_migrations表就行(如果没在里面的话),或者直接让ShipIt跳过它。
2. 修改迁移文件,添加安全检查(推荐)
为了避免以后再出这种问题,最好给迁移加个字段存在性判断,同时我还发现你迁移里有个小bug,一起改了:
class BreakUpFullnameInUsers < ActiveRecord::Migration[5.2] def up # 先检查字段是否存在,再执行添加 add_column :users, :first_name, :string unless column_exists?(:users, :first_name) add_column :users, :last_name, :string unless column_exists?(:users, :last_name) # 只有name字段存在时,才执行拆分逻辑 if column_exists?(:users, :name) User.all.each do |user| # 原来的name.split是错的,应该用user.name! fn, ln = user.name.split(' ', 2) user.update(first_name: fn, last_name: ln) end remove_column :users, :name end end def down add_column :users, :name, :string unless column_exists?(:users, :name) if column_exists?(:users, :first_name) && column_exists?(:users, :last_name) User.all.each do |user| # 原来的name.first_name也是错的,要改成user.first_name n = [user.first_name, user.last_name].join(' ') user.update(name: n) end remove_column :users, :first_name remove_column :users, :last_name end end end
这个修改后,不管生产环境有没有执行过这个迁移,都能安全运行,不会再报重复列的错误,同时还修复了你代码里引用实例属性的bug(本地可能没触发,但生产环境说不定会出问题)。
3. 手动更新schema_migrations表
如果确认迁移已经执行过,只是schema_migrations里没记录,可以登录生产数据库,执行这条SQL:
INSERT INTO schema_migrations (version) VALUES ('20180507080705');
之后再重新部署,ShipIt就会跳过这个迁移了。
4. 回滚后重新迁移(谨慎操作!)
如果生产环境的数据允许(比如可以临时停机备份),你可以先回滚这个迁移,再重新执行:
rails db:rollback VERSION=20180507080705 rails db:migrate
但这个操作会修改生产数据,一定要先备份数据库再操作!
内容的提问来源于stack exchange,提问作者Emma O.

