Модели Rails: как бы вы создали предопределенный набор атрибутов?
Я пытаюсь найти лучший способ разработки модели рельсов. В качестве примера предположим, что я строю базу данных символов, которая может иметь несколько разных фиксированных атрибутов. Например:
Character
- Morality (may be "Good" or "Evil")
- Genre (may be "Action", "Suspense", or "Western")
- Hair Color (may be "Blond", "Brown", or "Black")
... и т.д.
Итак, для модели Character есть несколько атрибутов, в которых я хочу в основном иметь фиксированный список возможных выборов.
Я хочу, чтобы пользователи могли создавать символ, и в форме я хочу, чтобы они выбирали один из каждого из доступных параметров. Я также хочу, чтобы пользователи могли искать каждый из этих атрибутов... (т.е. "Покажите мне персонажи, которые являются" хорошими ", из жанра" Suspense "и имеют" коричневые "волосы).
Я могу придумать пару способов сделать это...
1: Создайте строку для каждого атрибута и подтвердите ограниченный ввод.
В этом случае я бы определил строковый столбец "Моральность" в таблице символов, затем сохранил константу класса с указанными в ней опциями, а затем проверил бы против этой константы класса.
Поиск хороших символов будет выглядеть как Character.where(:morality=>'Good')
.
Это приятно и просто, недостатком является то, что я хотел бы добавить в атрибут более подробную информацию, например, для описания "Хорошего" и "Злого" и страницы, где пользователи могли бы просматривать все символы для данной морали.
2: Создайте модель для каждого атрибута
В этом случае Character belongs_to Morality
была бы модель Morality
и таблица moralities
с двумя записями в ней: Morality id:1, name:Good
и т.д.
Поиск хороших символов будет выглядеть как Morality.find_by_name('Good').characters
... или
Character.where(:morality=> Morality.find(1)
.
Это отлично работает, но это означает, что у вас есть несколько таблиц, которые существуют только для хранения небольшого количества предопределенных атрибутов.
3: создать модель STI для атрибутов
В этом случае я мог бы сделать то же самое, что и # 2, за исключением создания общей таблицы CharacterAttributes, а затем подкласса для "MoralityAttribute" и "GenreAttribute" и т.д. Это делает только одну таблицу для многих атрибутов, иначе это кажется примерно так же, как идея № 2.
Итак, это три способа, которые я могу придумать для решения этой проблемы.
Мой вопрос: как бы вы это реализовали и почему?
Вы бы использовали один из подходов выше, и если да, то какой? Вы сделали бы что-то другое? Мне было бы особенно интересно услышать соображения производительности для подхода, который вы бы предприняли. Я знаю, что это широкий вопрос, спасибо за любой вклад.
EDIT:
Я добавляю Bounty 250 (более 10% от моей репутации!!) по этому вопросу, потому что я действительно мог бы использовать более расширенное обсуждение опций/против/вариантов. Я дам upvotes всем, кто весит с чем-то конструктивным, и если кто-то может дать мне действительно солидный пример того, какой подход они берут, и ПОЧЕМУ он будет стоить +250.
Я действительно мучительно отношусь к дизайну этого аспекта моего приложения, и теперь пришло время его реализовать. Заранее благодарим за полезное обсуждение!
ЗАКЛЮЧИТЕЛЬНОЕ ПРИМЕЧАНИЕ:
Спасибо всем за ваши продуманные и интересные ответы, все они хороши и были очень полезны для меня. В конце концов (пришло прямо перед тем, как щедрость истекло!) Я действительно оценил ответ Blackbird07. Хотя все предлагали хорошие предложения, для меня лично он был самым полезным. Раньше я не знал об идее перечисления, и, глядя в него, я нахожу, что он решает многие проблемы, которые у меня возникают в моем приложении. Я бы призвал всех, кто обнаруживает этот вопрос, прочитать все ответы, есть много хороших подходов.
Ответы
Ответ 1
Проще говоря, вы спрашиваете, как перечислять атрибуты ActiveRecord. Существует много обсуждений в Интернете и даже на SO для использования перечислений в приложениях rails, например. здесь, здесь или здесь, чтобы назвать несколько.
Я никогда не использовал один из многих драгоценных камней для перечислений, , но active_enum gem звучит особенно подходящим для вашего случая использования. Он не имеет недостатков набора атрибутов, поддерживаемых activerecord, и делает поддержание значений атрибутов куском торта. Он даже поставляется с помощниками формы для formtastic или простой формы (который, как я полагаю, может помочь вам в выборе атрибутов в вашем поиске персонажей).
Ответ 2
Я предполагаю, что у вас будет больше, чем несколько из этих атрибутов с множественным выбором, и хотел бы сохранить порядок вещей.
Я бы рекомендовал хранить его в базе данных только , если вы хотите изменить варианты во время выполнения, иначе это быстро станет удар производительности; Если модель имеет три таких атрибута, для ее возврата потребуется четыре вызова из базы данных, а не один.
Hardcoding выбора в валидации - это быстрый способ, но он становится утомительным для поддержки. Вы должны убедиться, что каждый подобный валидатор и раскрывающийся список и т.д. Используют соответствующие значения. И это становится довольно тяжелым и громоздким, если список становится длинным. Это практично, если у вас есть 2-5 вариантов, которые действительно не будут сильно меняться, например male, female, unspecified
Что я могу рекомендовать, так это то, что вы используете файл конфигурации конфигурации YAML. Таким образом, у вас может быть один аккуратный документ для всех ваших вариантов
# config/choices.yml
morality:
- Good
- Evil
genre:
- Action
- Suspense
- Western
hair_color:
- Blond
- Brown
- Black
Затем вы можете загрузить этот файл в константу как Hash
# config/initializers/load_choices.rb
Choices = YAML.load_file("#{Rails.root}/config/choices.yml")
Используйте его в своих моделях;
# app/models/character.rb
class Character < ActiveRecord::Base
validates_inclusion_of :morality, in: Choices['morality']
validates_inclusion_of :genre, in: Choices['genre']
# etc…
end
Использовать их в представлениях;
<%= select @character, :genre, Choices['genre'] %>
и т.д...
Ответ 3
Если изменение любого из этих атрибутов будет сильно привязано к изменению кода (т.е. когда вводится новый цвет волос, создается новая страница или выполняется новое действие), то я бы сказал добавьте их как хэш строки (вариант 1). Вы можете сохранить их в модели символов в качестве завершенных хэшей с другими метаданными.
class Character < ActiveRecord::Base
MORALITY = {:good => ['Good' => 'Person is being good'], :evil => ['Evil' => 'Person is being Evil']}
...
end
Character.where(:morality => Character::MORALITY[:good][0])
Изменить, чтобы добавить код из комментария:
Учитывая Character::MORALITY = {:good => {:name => 'Good', :icon => 'good.png'}, ...
- Character::MORALITY.each do |k,v|
= check_box_tag('morality', k.to_s)
= image_tag(v[:icon], :title => v[:name])
= Character::MORALITY[@a_character.morality.to_sym][:name]
Ответ 4
Мое предложение - использовать базу данных NoSQL, такую как MongoDB.
MongoDB поддерживает встроенные документы. Встроенный документ сохраняется в той же записи, что и родительский. Так что это очень быстро для поиска, это похоже на доступ к общему полю. Но встроенные документы могут быть очень богатыми.
class Character
include Mongoid::Document
embeds_one :morality
embeds_many :genres
embeds_one :hair_colour
index 'morality._type'
index 'genres._type'
end
class Morality
include Mongoid::Document
field :name, default: 'undefined'
field :description, default: ''
embedded_in :character
end
class Evil < Morality
include Mongoid::Document
field :name, default: 'Evil'
field :description,
default: 'Evil characters try to harm people when they can'
field :another_field
end
class Good < Morality
include Mongoid::Document
field :name, default: 'Good'
field :description,
default: 'Good characters try to help people when they can'
field :a_different_another_field
end
Операции:
character = Character.create(
morality: Evil.new,
genres: [Action.new, Suspense.new],
hair_colour: Yellow.new )
# very very fast operations because it is accessing an embed document
character.morality.name
character.morality.description
# Very fast operation because you can build an index on the _type field.
Character.where('morality._type' => 'Evil').execute.each { |doc| p doc.morality }
# Matches all characters that have a genre of type Western.
Character.where('genres._type' => 'Western')
# Matches all characters that have a genre of type Western or Suspense.
Character.any_in('genres._type' => ['Western','Suspense'])
Этот подход имеет то преимущество, что добавление нового типа Морали просто добавляет новую модель, которая наследуется от Морали. Вам не нужно ничего менять.
Добавление новых типов нравственности не приводит к снижению производительности. Индекс заботится о быстрых операциях запроса.
Доступ к встраиваемым полям выполняется очень быстро. Это похоже на доступ к общему полю.
Преимущество такого подхода только в YML файле заключается в том, что вы можете иметь очень богатые встраиваемые документы. Каждый из этих документов может отлично развиваться в соответствии с вашими потребностями. Нужно поле описания? добавьте его.
Но я бы объединил два варианта. Файл YML может быть очень полезен для использования ссылки, которую вы можете использовать, например, в блоках выбора. Хотя вложение документов дает вам необходимую гибкость.
Ответ 5
Я буду следовать двум принципам: СУХОЙ, счастье разработчиков над кодом усложняет.
Прежде всего, предопределенные данные символов будут в модели как константа.
Во-вторых, о валидации, мы сделаем здесь немного метапрограммирования, а также выполним поиск по областям.
#models/character.rb
class Character < ActiveRecord::Base
DEFAULT_VALUES = {:morality => ['Good', 'Evil'], :genre => ['Action', 'Suspense', 'Western'], :hair_color => ['Blond', 'Brown', 'Black']}
include CharacterScopes
end
#models/character_scopes.rb
module CharacterScopes
def self.included(base)
base.class_eval do
DEFAULT_VALUES.each do |k,v|
validates_inclusion_of k.to_sym, :in => v
define_method k do
where(k.to_sym).in(v)
end
# OR
scope k.to_sym, lambda {:where(k.to_sym).in(v)}
end
end
end
end
#app/views/characters/form.html
<% Character::DEFAULT_VALUES.each do |k,v] %>
<%= select_tag :k, options_from_collection_for_select(v) %>
<% end %>
Ответ 6
Для случая с несколькими значениями один из вариантов - использовать битовые поля, реализованные в flagShihTzu. Это сохраняет несколько флагов в одном целочисленном поле.