Rails Models: how would you create a pre-defined set of attributes? - ruby-on-rails

I'm trying to figure out the best way to design a rails model. For purposes of the example, let's say I'm building a database of characters, which may have several different fixed attributes. For instance:
Character
- Morality (may be "Good" or "Evil")
- Genre (may be "Action", "Suspense", or "Western")
- Hair Color (may be "Blond", "Brown", or "Black")
... and so on.
So, for the Character model there are several attributes where I want to basically have a fixed list of possible selections.
I want users to be able to create a character, and in the form I want them to pick one from each of the available options. I also want to be able to let users search using each of these attributes... ( ie, "Show me Characters which are 'Good', from the 'Suspense' genre, and have 'Brown' hair).
I can think of a couple ways to do this...
1: Create a string for each attribute and validate limited input.
In this case I would define an string column "Morality" on the character table, then have a class constant with the options specified in it, and then validate against that class constant.
Finding good characters would be like Character.where(:morality=>'Good').
This is nice and simple, the downside is if I wanted to add some more detail to the attribute, for instance to have a description of "Good" and "Evil", and a page where users could view all the characters for a given morality.
2: Create a model for each attribute
In this case Character belongs_to Morality, there would be a Morality model and a moralities table with two records in it: Morality id:1, name:Good etc.
Finding good characters would be like Morality.find_by_name('Good').characters... or
Character.where(:morality=> Morality.find(1).
This works fine, but it means you have several tables that exist only to hold a small number of predefined attributes.
3: Create a STI model for attributes
In this case I could do the same as #2, except create a general "CharacterAttributes" table and then subclass it for "MoralityAttribute" and "GenreAttribute" etc. This makes only one table for the many attributes, otherwise it seems about the same as idea #2.
So, those are the three ways I can think of to solve this problem.
My question is, how would you implement this, and why?
Would you use one of the approaches above, and if so which one? Would you do something different? I'd especially be interested to hear performance considerations for the approach you would take. I know this is a broad question, thank you for any input.
EDIT:
I'm adding a Bounty of 250 (more than 10% of my reputation!!) on this question because I could really use some more extended discussion of pros / cons / options. I'll give upvotes to anyone who weighs in with something constructive, and if someone can give me a really solid example of which approach they take and WHY it'll be worth +250.
I'm really agonizing over the design of this aspect of my app and it's now time to implement it. Thanks in advance for any helpful discussion!!
FINAL NOTE:
Thank you all for your thoughtful and interesting answers, all of them are good and were very helpful to me. In the end (coming in right before the bounty expired!) I really appreciated Blackbird07's answer. While everyone offered good suggestions, for me personally his was the most useful. I wasn't really aware of the idea of an enum before, and since looking into it I find it solves many of the issues I've been having in my app. I would encourage everyone who discovers this question to read all the answers, there are many good approaches offered.

I assume that you are going to have more than a few of these multiple-choice attributes, and would like to keep things tidy.
I would recommend the store it in the database approach only if you want to modify the choices at runtime, otherwise it would quickly become a performance hit; If a model has three such attributes, it would take four database calls instead of one to retreive it.
Hardcoding the choices into validations is a fast way, but it becomes tedious to maintain. You have to make sure that every similar validator and drop-down list etc. use matching values. And it becomes quite hard and cumbersome if the list becomes long. It's only practical if you have 2-5 choices that really won't change much, like male, female, unspecified
What I'd recommend is that you use a configuration YAML file. This way you can have a single tidy document for all your choices
# config/choices.yml
morality:
- Good
- Evil
genre:
- Action
- Suspense
- Western
hair_color:
- Blond
- Brown
- Black
Then you can load this file into a constant as a Hash
# config/initializers/load_choices.rb
Choices = YAML.load_file("#{Rails.root}/config/choices.yml")
Use it in your models;
# app/models/character.rb
class Character < ActiveRecord::Base
validates_inclusion_of :morality, in: Choices['morality']
validates_inclusion_of :genre, in: Choices['genre']
# etc…
end
Use them in views;
<%= select #character, :genre, Choices['genre'] %>
etc…

Put simply, you're asking how to enumerate ActiveRecord attributes. There are a lot of discussions around the web and even on SO for using enums in rails applications, e.g. here, here or here to name a few.
I never used one of the many gems there are for enums, but active_enum gem sounds particularly suited for your use case. It doesn't have the downsides of an activerecord-backed attribute set and makes maintenance of attribute values a piece of cake. It even comes with form helpers for formtastic or simple form (which I assume could help you for attribute selection in your character search).

If a change in any of these attributes would be strongly tied to a change in the code (ie: When a new Hair Color is introduced, a new page is created or a new action is implemented), then I'd say add them as a string hash (option 1). You could store them in the Character model as a finalized hashes with other meta-data.
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])
Edit to add the code from comment:
Given 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]

My suggestion is to use a NoSQL database such as MongoDB.
MongoDB support embedded documents. An embedded document is saved in the same entry as the parent. So it is very fast for retrieval, it is like accessing a common field. But embed documents can be very rich.
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
Operations:
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'])
This approach has the advantage that adding a new type of Morality is just adding a new Model that inherits from Morality. You don't need to change anything else.
Adding new Morality types do not have any performance penalty. The index take care of maintaing fast query operations.
Accessing the embed fields is very fast. It is like accessing a common field.
The advantage of this approach over just a YML file is that you can have very rich embed documents. Each of these documents can perfectly grow to your needs. Need a description field? add it.
But I would combine the two options. The YML file could be very useful for having a reference that you can use in Select boxes for example. While having embeds document gives you the desired flexibility.

I'll follow 2 principles: DRY, developers happiness over code complicate.
First of all, the predefined Character data will be in the model as a constant.
The second is about validation, we will do a bit metaprogramming here, as well as searching with scopes.
#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 %>

For the multiple values case, one option is to use bit fields as implemented in the FlagShihTzu gem. This stores a number of flags in a single integer field.

Related

How to use `first_or_initialize` step with `accepts_nested_attributes_for` - Mongoid

I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.
Basically, I'd like to do something like:
# override authors_attributes
def authors_attributes(attrs)
attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
super(attrs)
end
But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).
I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).
Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...
The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.
With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.
This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.
If I have a model Paper:
class Paper
include Mongoid::Document
embeds_many :authors
accepts_nested_attributes_for :authors
field :title, type: String
end
And a model Author embedded in Paper:
class Author
include Mongoid::Document
embedded_in :paper, inverse_of: :authors
field :name, type: String
end
I can do this in the console:
> paper = Paper.create(title: "My Paper")
> paper.authors_attributes = [ {name: "Raviolicode"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]
> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]
As you can see, I can update and add authors in the same authors_attributes hash.
For more information see Mongoid nested_attributes article
I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:
accepts_nested_attributes_for :authors, reject_if: :check_author
def check_author(attrs)
if existing = Author.where(label: attrs['label']).first
self.authors << existing
true
else
false
end
end
This still seems like a hack, but it works in Mongoid as well...

possible to have model in Mongoid/Rails with a field that varies in number?

I'm trying to create a questionnaire app in Rails using Mongoid. I keep stumbling over the database setup because I'm new to things.
I want the users to be able to create questions with the possibility for varying numbers of answers. Some questions might have two possibilities: true, false. Some might have four or five possibilities.
So I've tried to create a question model and an answer model, then embed the answers in the question. I've tried a model with question:string answer-a:string answer-b:string answer-c:string and so on. But both approaches seem stupid and unwieldy.
Is there a way to create a model that allows someone to create a question field and an answer field, but such that the answer field can have multiples? So, create question, add an answer, and keep adding answers until they've finished their multiple choice?
If answers are just strings then you could use an array field:
class Question
include Mongoid::Document
#...
field :answers, :type => Array
end
If answers have some internal structure (perhaps you want to track when they were created or changed or whatever), then you could use embeds_many and two models:
class Question
include Mongoid::Document
#...
embeds_many :answers
end
class Answer
include Mongoid::Document
#...
embedded_in :question
end
Either one will let you work with q.answers in a natural list-like way so rendering such things is a simple matter of <% q.answers.each do |a| %> and you could shuffle the answers to display them in a random order.
If you want dynamically generated forms for nested models, I suggest following this RailsCast: http://railscasts.com/episodes/196-nested-model-form-part-1
The RailsCast method uses application helpers to dynamically generate the new objects.
I tend to prefer #mu's method of using jQuery to create form elements. The nice thing about Rails is that, when passing nested attributes, you can give it whatever index you want. And so I'll generate a new form with an index of, say, Time.now.to_s and no ID parameter. And so my controller receives a parameter that looks like this:
{"question" => {"answers_attributes" => { "1234567" => { "description" => "New answer" } } } }

Parse before storing in MVC

I'm getting started with parsing data and getting some structure from user supplied strings (mostly pulling out digits and city names).
I've run a bit of code in the ruby interpreter, and now I want to use that same code in a web application.
I'm struggling as to where in the code my parsing should be, or how it is structured.
My initial instinct was that it belongs in the model, because it is data logic. For example, does the entry have an integer, does it have two integers, does it have a city name, etc. etc.
However, my model would need to inherit both ActiveRecord, and Parslet (for the parsing), and Ruby apparently doesn't allow multiple inheritance.
My current model is looking like this
#concert model
require 'parslet'
class concert < Parlset::Parser
attr_accessible :date, :time, :city_id, :band_id, :original_string
rule(:integer) {match('[0-9]').repeat(1)}
root(:integer)
end
Really not much there, but I think I'm stuck because I've got the structure wrong and don't know how to connect these two pieces.
I'm trying to store the original string, as well as components of the parsed data.
I think what you want is:
#concert model
require 'parslet'
class concert < ActiveRecord::Base
before_save :parse_fields
attr_accessible :date, :time, :city_id, :band_id, :original_string
rule(:integer) {match('[0-9]').repeat(1)}
root(:integer)
private
def parse_fields
date = Parlset::Parser.method_on_original_string_to_extract_date
time = Parlset::Parser.method_on_original_string_to_extract_time
city_id = Parlset::Parser.method_on_original_string_to_extract_city_id
band_id = Parlset::Parser.method_on_original_string_to_extract_band_id
end
end
It looks to me as though you need several parsers (one for city names, one for digits). I would suggest that you create an informal interface for such parsers, such as
class Parser
def parse(str) # returning result
end
end
Then you would create several Ruby classes that each do a parse task in ./lib.
Then in the model, you'd require all these ruby classes, and put them to the task, lets say in a before_save hook or such.
As the author of parslet, I might add that parsing digits or city names is probably not the sweet spot for parslet. Might want to consider regular expressions there.

How do I get a value from a composite key table using a Rails model?

I have the following schema (* means primary key):
languages
id*
english_name
native_name
native_to_target_language
native_language_id*
target_language_id*
target_language_name
overview_text
(target_language_name is the name of the target language as written in the native language).
I want to get the value of target_language_name from the native_to_target_language table given values for native_language_id and target_language_id.
What is the best way to go about getting this? Use Composite Primary Keys from http://compositekeys.rubyforge.org/? Is there a standard way WITHOUT using a raw SQL query?
Its not very clear if you need CRUD operations. If you want to find then you can do the following:
NativeToTargetLanguage.find(:all, :conditions => {
:native_language_id => native_language_id,
:target_language_id => target_language_id }
)
Instead of rolling your own translation system, have you investigated any "off the shelf" varieties?
For example there's Globalize which does a lot of this for you.
Having a table with a compound key that represents a connection from one record in a table to another is going to be so much trouble. Generally you need to maintain an A<->B association as a pair of A->B and B->A varieties.
What about this as a general example:
class Language < ActiveRecord::Base
belongs_to :phrase
has_many :translations,
:through => :phrase
end
class Phrase < ActiveRecord::Base
has_many :translations
end
class Translation < ActiveRecord::Base
belongs_to :language
belongs_to :phrase
end
In this case Phrase is a kind of record representing the term to be translated. It could represent "French" or "English" or "Click Here" depending on the circumstances.
Using this structure it is straightforward to find the proper term to describe a language in any language you have defined.
For example, roughly:
<%= link_to(Language.find_by_code('fr').phrase.translation.find_by_language_id(session_language_id), '/fr') %>
It sounds like you might want something like Polymorphic Associations. This would require a separate table per language, which is (I think) what you are discussing.
However, it seems like there might be a better solution to your problem. Particularly, you might be able to come up with a better schema to solve this (unless the database is already in use and you are creating a Rails app to try to access it).
Can you describe the overall problem you are attempting to solve?

mongodb data design question

I'm trying my first application with mongodb on Rails using mongo_mapper and I'm weighing my options on an STI model like below.
It works fine, and I will of course add to this in more ways than I can currently count, I just curious if I wouldn't be better off with Embedded Documents or some such.
I'd like my models to share as much as possible, IE since they all inherit certain attributes, a shared form partial on property/_form.html.erb... in addition to their own unique form elements etc. I know the views will differ but I'm not sure on the controllers yet, as I could use property controller I assume for most things? And I'm sure it will get more complex as I go along.
Any pointers resources and/or wisdom (pain saving tips) would be greatly appreciated
property.rb
class Property
include MongoMapper::Document
key :name, String, :required => true
key :_type, String, :required => true
key :location_id, Integer, :required => true
key :description, String
key :phone, String
key :address, String
key :url, String
key :lat, Numeric
key :lng, Numeric
key :user_id, Integer, :required => true
timestamps!
end
restaurant
class Restaurant < Property
key :cuisine_types, Array, :required => true
end
bar
class Bar < Property
key :beers_on_tap, Array
end
Don't be afraid of more models, the idea of OO is to be able to cut up your concerns into tiny pieces and then treat each of them in the way they need to be treated.
For example, your Property model seems to be doing a whole lot. Why not split out the geo stuff you've got going on into an EmbeddedDocument (lat, lng, address, etc)? That way your code will remain simpler and more readable.
I use this sort of STI myself and I find it makes my code much simpler and more useable. One of the beauties of using a DB like Mongo is that you can do very complex STI like this and still have a manageable collection of data.
Regarding your cuisine_types and beers_on_tap etc, I think those are fine concepts. It might be useful to have Cuisine and Beer models too, so your database remains more normalized (a concept that is easy to lose in Mongo). e.g.:
class Bar < Property
key :beer_ids, Array
many :beers, :in => :beer_ids
end
class Beer
include MongoMapper:Document
key :name, String
end
Do you expect to return both Restaurants and Bars in the same query?
If not, you might want to reconsider having them derive from a base type.
By default, Mongo_Mapper is going to put both Restaurants and Bars in a single collection. This could hamper performance and make things harder to scale in the future.
Looking through some of the Mongo_Mapper code, it looks like you might be able to set this on the fly with set_collection_name.

Resources