13

在我目前正在 Rails 4.0.0beta1 下开发的项目中,我需要基于用户的身份验证,其中每个用户都可以链接到一个实体。我对rails有点陌生,这样做有一些麻烦。

模型如下:

class User < ActiveRecord::Base
end

class Agency < ActiveRecord::Base
end

class Client < ActiveRecord::Base
  belongs_to :agency
end

我需要的是用户能够链接到代理机构或客户,但不能同时链接到两者(这两个是我将要调用的实体)。它可以完全没有链接,最多只有一个链接。

我寻找的第一件事是如何在 Rails 中进行多表继承(MTI)。但是有些事情阻止了我:

  • 它不是开箱即用的
  • 对于像我这样的新手来说,MTI 看起来有点难以实施
  • 实施解决方案的 gem 似乎过时了,要么太复杂,要么不完整
  • 宝石可能会在 rails4 下损坏,因为它们已经有一段时间没有更新了

所以我寻找另一种解决方案,我发现了多态关联

从昨天开始,我就一直在研究这个问题,即使在Rails 多态 has_many :throughActiveRecord、has_many :through 和多态关联的帮助下,我也花了一些时间让它工作

我设法使上述问题中的示例起作用,但花了一段时间,我终于遇到了两个问题:

  1. 如何将 user 中的关系转换为 has_one 关联并能够“盲目地”访问链接实体?
  2. 如何设置约束以使用户不能拥有多个实体?
  3. 有没有更好的方法来做我想做的事?
4

2 回答 2

15

这是一个完整的工作示例:

迁移文件:

class CreateUserEntities < ActiveRecord::Migration
  def change
    create_table :user_entities do |t|
      t.integer :user_id
      t.references :entity, polymorphic: true

      t.timestamps
    end

    add_index :user_entities, [:user_id, :entity_id, :entity_type]
  end
end

型号:

class User < ActiveRecord::Base
  has_one :user_entity

  has_one :client, through: :user_entity, source: :entity, source_type: 'Client'
  has_one :agency, through: :user_entity, source: :entity, source_type: 'Agency'

  def entity
    self.user_entity.try(:entity)
  end

  def entity=(newEntity)
    self.build_user_entity(entity: newEntity)
  end
end

class UserEntity < ActiveRecord::Base
  belongs_to :user
  belongs_to :entity, polymorphic: true

  validates_uniqueness_of :user
end

class Client < ActiveRecord::Base
  has_many :user_entities, as: :entity
  has_many :users, through: :user_entities
end

class Agency < ActiveRecord::Base
  has_many :user_entities, as: :entity
  has_many :users, through: :user_entities
end

如您所见,我添加了一个 getter 和一个 setter,我将其命名为“实体”。这是因为has_one :entity, through: :user_entity引发以下错误:

ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot have a has_many :through association 'User#entity' on the polymorphic object 'Entity#entity' without 'source_type'. Try adding 'source_type: "Entity"' to 'has_many :through' definition.

最后,这是我设置的测试。我给它们是为了让每个人都明白知道你可以在这些对象之间设置和访问数据。我不会详细介绍我的 FactoryGirl 模型,但它们非常明显

require 'test_helper'

class UserEntityTest < ActiveSupport::TestCase

  test "access entity from user" do
    usr = FactoryGirl.create(:user_with_client)

    assert_instance_of client, usr.user_entity.entity
    assert_instance_of client, usr.entity
    assert_instance_of client, usr.client
  end

  test "only right entity is set" do
    usr = FactoryGirl.create(:user_with_client)

    assert_instance_of client, usr.client
    assert_nil usr.agency
  end

  test "add entity to user using the blind rails method" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    usr.build_user_entity(entity: client)
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id
  end

  test "add entity to user using setter" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    usr.client = client
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id
  end

  test "add entity to user using blind setter" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    usr.entity = client
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id
  end

  test "add user to entity" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    client.users << usr

    result = UserEntity.where(entity_id: client.id, entity_type: 'client')

    assert_equal 1, result.size
    assert_equal usr.id, result.first.user_id
  end

  test "only one entity by user" do

    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)
    agency = FactoryGirl.create(:agency)

    usr.agency = agency
    usr.client = client
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id

  end

  test "user uniqueness" do

    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)
    agency = FactoryGirl.create(:agency)

    UserEntity.create!(user: usr, entity: client)

    assert_raise(ActiveRecord::RecordInvalid) {
      UserEntity.create!(user: usr, entity: agency)
    }

  end

end

我希望这可以对某人有所帮助。我决定将整个解决方案放在这里,因为与 MTI 相比,它在我看来是一个不错的解决方案,而且我认为设置类似的东西不应该花费太多时间。

于 2013-03-14T17:45:42.600 回答
0

上面的答案给我带来了一些麻烦。验证唯一性时使用列名而不是模型名。将 validates_uniqueness_of :user 更改为 validates_uniqueness_of :user_id。

于 2013-08-15T22:16:18.023 回答