Объединить области модели по-Rails

#ruby-on-rails

#ruby-on-rails

Вопрос:

Я случайно заметил, что две мои модели имеют некоторое сходство. Их имена GameItem и OwnedItem. Игровой элемент — это просто элемент игры, в то время как OwnedItem показывает, есть ли у игрока этот предмет, есть ли он в его инвентаре или на складе и многое другое. Мои модели теперь похожи (я удалил проверки и некоторый нерелевантный код для простоты) :

 class OwnedItem < ActiveRecord::Base
    belongs_to :user
    belongs_to :game_item
    belongs_to :ownable, :polymorphic => true  # [warehouse|inventory]

  scope :equipped, where(:is_equipped => 1).includes(:game_item)

  scope :item, lambda { |item_type|
    joins(:game_item).
    where("game_items.item_type = ?", item_type ).
    limit(1)
  }

  scope :inventory, where(:ownable_type => 'Inventory')
  scope :warehouse, where(:ownable_type => 'Warehouse') 
end



class GameItem < ActiveRecord::Base 

  scope :can_be_sold, where(:is_sold => 1)

  scope :item_type, lambda { |item_type|
    where("game_items.item_type = ?", item_type )
  } 

  scope :item_types, lambda { |item_types|
    where("game_items.item_type IN (?)", item_types )
  }   

  scope :class_type, lambda { |class_type|
    where("game_items.class_type = ?", class_type )
  }

  scope :grade, lambda { |grade|
    where("game_items.grade = ?", grade )
  }
end
  

Обратите внимание на проблему с game_item.item_type. Я ссылаюсь на нее в модели owned_item, тем самым нарушая инкапсуляцию и повторяясь. Как я могу на самом деле сделать что-то вроде :

 user.inventory.owned_items.item_type('Weapon').equipped
  

то есть, фактически не добавляя повторяющийся код в мою модель OwnedItem, но получая эту информацию из модели GameItem?

Ответ №1:

Я думаю, вы определили взаимосвязи здесь таким образом, что это вызовет у вас проблемы. Возможно, вы обнаружите, что лучше использовать простую модель соединения пользователя с элементом, что-то вроде этого:

 class User < ActiveRecord::Base
  has_many :owned_items
  has_many :game_items, :through => :owned_items
end

class OwnedItem < ActiveRecord::Base
  belongs_to :user
  belongs_to :game_item

  # Has 'location' field: 'warehouse' or 'inventory'
end

class GameItem < ActiveRecord::Base
  has_many :owned_items
  has_many :users, :through => :owned_items
end
  

Это обычная схема, в которой у вас есть пользователи и какая-то вещь, экземпляром которой они будут владеть. Таблица отношений в середине, OwnedItem, используется для установления, среди прочего, любых уникальных характеристик этого конкретного экземпляра GameItem, а также его местоположения относительно пользователя.

Как правило, такого рода структура позволяет избежать использования полиморфных ассоциаций, которые могут вызвать проблемы при слишком случайном использовании. По возможности старайтесь избегать полиморфных ассоциаций, если они не находятся на самом краю ваших отношений. Размещение их в середине значительно усложняет объединения и значительно усложняет настройку индексов.

В качестве примечания к оригиналу, вы можете свернуть большую часть этого в простую область, которая использует метод хэширования для where :

 scope :with_item_type, lambda { |types|
  where('game_items.item_type' => types)
}
  

Это примет либо аргумент array, либо аргумент string и будет использовать IN или = соответственно. На самом деле это довольно удобно делать таким образом, потому что вам не нужно будет запоминать, какой из них использовать.

Комментарии:

1. эй, тадман, спасибо за ответ 🙂 На самом деле у меня есть модель пользователя с теми же ассоциациями, что и вы предлагаете (я просто не включил ее сюда, но вы можете увидеть, что belongs_to используется в моей модели ownedItem ). Я также пытался использовать has_many owned_items в game_item, но все равно не смог сделать что-то вроде user.inventory.owned_items.game_items.item_type(‘Оружие’).equipped. Здесь мне также понадобились полиморфные ассоциации,, потому что элемент может находиться на складе или в инвентаре.

2. Область, которую вы предлагаете, действительно довольно удобна, ваш ответ определенно заслуживает 1, но у меня все равно была бы та же проблема с инкапсуляцией для модели ownedItem. Мне нужно было бы создать отдельную область, которая включает модель gameItem для проверки типов :/

3. Обычно причина, по которой у вас две разные модели, заключается в том, что объекты, которые они представляют, достаточно различны, чтобы требовать этого. Если «инвентарь» на самом деле не так уж сильно отличается от «склада» с точки зрения данных, почему вы не можете объединить их в единую модель с атрибутом индикатора местоположения? Если у них есть незначительные различия, почему бы не использовать STI? Полиморфные ассоциации обычно являются признаком того, что что-то не так.

4. Вы определенно не являетесь поклонником полиморфизма rails 🙂 Как у меня есть сейчас, игровой предмет имеет ownable_type, где я могу видеть, принадлежит ли предмет инвентарю или складу. И действительно, некоторые из аналогичных функциональных возможностей существуют в модели с именем Storable. Кажется, до сих пор это работало нормально, но я определенно принимаю ваши слова во внимание. Было бы утомительно менять сейчас, но я всегда стремлюсь к лучшему коду.

5. Я не возражаю против использования polymorphic , если оно находится на краю вашей структуры данных, но если оно посередине, это вызывает проблемы. Во-первых, это делает ваши JOIN инструкции в два раза сложнее, для ваших индексов требуются составные ключи, и вы не можете использовать внешние ключи для обеспечения целостности данных. Они предназначены для использования в качестве последнего средства, на самом деле, или в качестве меры удобства, когда альтернатива не намного быстрее.