You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

部署Rails应用时MySQL报重复列名错误,如何解决?

解决ShipIt部署Rails应用时的ActiveRecord重复列错误

刚碰到这个报错的时候我也头大,先给你理清楚问题出在哪,再给几个靠谱的解决办法:

首先看报错:

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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:04:21