has_one через и полиморфные ассоциации по многотабличному наследованию

В проекте, который я сейчас разрабатываю под 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 выглядело довольно сложно реализовать для новичка, такого как я
  • жемчужины, реализующие решения, казались старыми и либо слишком сложными, либо неполными
  • драгоценные камни, вероятно, сломались бы под рельсами4, поскольку они не обновлялись какое-то время

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

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

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

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

person Crystark    schedule 14.03.2013    source источник


Ответы (2)


Вот полностью рабочий пример:

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

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

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

person Crystark    schedule 14.03.2013
comment
@Crystark При тестировании указанного выше файла NameError я получаю следующую ошибку: неинициализированная константа UserWithClient - person Javier Lopez; 06.02.2016

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

person Margaret Scott    schedule 15.08.2013