Подтвердить что ты не робот

Модели 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. Хотя все предлагали хорошие предложения, для меня лично он был самым полезным. Раньше я не знал об идее перечисления, и, глядя в него, я нахожу, что он решает многие проблемы, которые у меня возникают в моем приложении. Я бы призвал всех, кто обнаруживает этот вопрос, прочитать все ответы, есть много хороших подходов.

4b9b3361

Ответ 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. Это сохраняет несколько флагов в одном целочисленном поле.