has_one through и полиморфные ассоциации над наложением нескольких таблиц

В проекте, который я сейчас разрабатываю под rails 4.0.0beta1, у меня возникла необходимость в пользовательской аутентификации, в которой каждый пользователь может быть связан с сущностью. Я новичок в рельсах и испытываю некоторые неприятности.

Модель выглядит следующим образом:

class User < ActiveRecord::Base
end

class Agency < ActiveRecord::Base
end

class Client < ActiveRecord::Base
  belongs_to :agency
end

Мне нужно, чтобы пользователь мог ссылаться как на агентство, так и на клиента, но не на обоих (эти два - это то, что я буду называть сущностями). Он не может иметь никакой ссылки вообще и не более одной ссылки.

Первое, что я искал, - это как наследование Mutli-Table (MTI) в рельсах. Но некоторые вещи заблокировали меня:

  • он не был доступен из коробки
  • MTI выглядела довольно сложно для новичков, таких как я
  • камни, реализующие решения, казались старыми или слишком сложными или не полными
  • драгоценные камни, вероятно, сломались бы под рельсами4, поскольку они не обновлялись какое-то время

Поэтому я искал другое решение, и я нашел полиморфные ассоциации.

Я был на этом со вчерашнего дня и занял некоторое время, чтобы заставить его работать даже с помощью Rails polymorphic has_many: through и ActiveRecord, has_many: through и Polymorphic Associations

Мне удалось привести примеры из вышеприведенного вопроса, но потребовалось некоторое время, и у меня, наконец, возникли две проблемы:

  1. Как преобразовать отношения в пользователе в ассоциацию has_one и иметь доступ к "слепому" связанному объекту?
  2. Как установить ограничение, чтобы ни один пользователь не мог иметь более одного объекта?
  3. Есть ли лучший способ сделать то, что я хочу?

Ответ 1

Вот полный рабочий пример:

Файл миграции:

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

Как вы можете видеть, я добавил геттер и сеттер, которые я назвал "сущностью". Это потому, что 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, и я думаю, что не стоит брать кого-то столько времени, чтобы установить что-то подобное.

Ответ 2

Вышеупомянутый ответ дал мне некоторые проблемы. Используйте имя столбца вместо имени модели при проверке уникальности. Изменить validates_uniqueness_of: пользователь для validates_uniqueness_of: user_id.