ruby-on-rails Ruby on Rails has_many:through STI foreign key issue?

kb5ga3dv  于 2023-04-08  发布在  Ruby
关注(0)|答案(1)|浏览(168)

我有一个来自Devise gem的用户类。
我有一个admin_account类,它是从使用STI的user继承的。我有一个admin_task类。我有一个admin_task_assignment类。
我在尝试通过rails控制台创建管理任务分配时遇到外键错误。
t = AdminTaskAssignment.create(admin_account_id: 1, admin_task_id: 1)
我同时搜索AdminAccount.find(1)和AdminTask.find(1)来查找存在的记录。但我得到:
SQLite3::ConstraintException: FOREIGN KEY constraint failed (ActiveRecord::InvalidForeignKey) FOREIGN KEY constraint failed (SQLite3::ConstraintException)

class AdminAccount < User 
    has_many :admin_task_assignments,  foreign_key: 'admin_account_id'
    has_many :admin_tasks, through: :admin_task_assignments
end

class AdminTask < ApplicationRecord
    has_many :admin_task_assignments, foreign_key: 'admin_task_id'
    has_many :admin_accounts, through: :admin_task_assignments
    belongs_to :client
end

class AdminTaskAssignment < ApplicationRecord
    belongs_to :admin_account, foreign_key: 'admin_account_id'
    belongs_to :admin_task, foreign_key: 'admin_task_id'
end

class Client < ApplicationRecord
    has_many :admin_tasks
end

class User < ApplicationRecord
  attribute :type, :string
  validates :email, uniqueness: true

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

end
create_table "admin_accounts", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
end

create_table "admin_task_assignments", force: :cascade do |t|
    t.integer "admin_account_id", null: false
    t.integer "admin_task_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["admin_account_id"], name: "index_admin_task_assignments_on_admin_account_id"
    t.index ["admin_task_id"], name: "index_admin_task_assignments_on_admin_task_id"
end

create_table "admin_tasks", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "client_id", null: false
    t.string "name"
    t.string "task_type"
    t.index ["client_id"], name: "index_admin_tasks_on_client_id"
end
  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "type"
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

add_foreign_key "admin_task_assignments", "admin_accounts"
add_foreign_key "admin_task_assignments", "admin_tasks"
add_foreign_key "admin_tasks", "clients"
kcwpcxri

kcwpcxri1#

首先,从你的模型中删除所有的cruft:

  • 电子邮件验证已由Devise::Validatable处理
  • 不需要定义架构中存在的属性。
  • 如果外键选项可以从关联的名称派生出来,并且过于明确也不会带来任何好处,那么就不需要配置外键选项。
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

class AdminAccount < User
  has_many :admin_task_assignments
  has_many :admin_tasks, 
    through: :admin_task_assignments
end

class AdminTaskAssignment < ApplicationRecord
  belongs_to :admin_account 
  belongs_to :admin_task
end

class AdminTask < ApplicationRecord
  has_many :admin_task_assignments
  has_many :admin_accounts, 
    through: :admin_task_assignments
  belongs_to :client
end

然后,让我们解决数据库模式。

  • 如果使用单表继承,则不需要或不想要admin_accounts表。ActiveRecord甚至不会使用它,因为AdminAccount继承自User。请删除它。
  • 您需要正确地设置外键,以便它们指向users表。

如果您还没有合并/推送这些模式更改,纠正这种情况的最简单方法是回滚到创建admin_accounts表之前,并修复/删除迁移:

class CreateAdminTaskAssignments < ActiveRecord::Migration[7.0]
  def change
    create_table :admin_task_assignments do |t|
      t.belongs_to :admin_account, 
        null: false, 
        foreign_key: { to_table: :users } # this is super important
      t.belongs_to :admin_task, null: false, foreign_key: true
      t.timestamps
      # optional -- add a unique index to ensure that the same task can't be assigned multiple times
      # t.index [:admin_account_id, :admin_task_id], unique: true
    end
  end
end

然后删除创建admin_accounts表的迁移。重新运行迁移,Bob会发现你已经被淘汰了。因为你正在删除表,所以你不必处理一堆带有垃圾数据的孤立记录。
如果你需要修复这个问题,你需要删除外键并替换为正确的外键:

class FixTaskAssignmentsForeignKeys < ActiveRecord::Migration[7.0]
  def change
    remove_foreign_key :admin_tasks, :admin_accounts
    add_foreign_key :admin_tasks, :users, 
     column: :admin_account_id
  end
end

请注意,如果您已经向应用添加了一堆数据,则admin_account_id列中可能已经有垃圾值,这些值实际上是来自已失效的admin_accounts表的id,而不是用户id。您可能需要截断该表。
然后,您可以删除admin_accounts表,因为不再有外键引用它:

class DropAdminAccounts < ActiveRecord::Migration[7.0]
  def change
    drop_table :admin_accounts do |t|
      t.timestamps 
    end
  end
end

相关问题