Model Inheritance in Ruby on Rails 3 - ruby-on-rails

Please help a newbie to choose the best way to implement inheritance in RoR3. I have:
-Person (address fields, birthdate, etc.)
-Player, inherits from Person (position, shoe_size, etc.)
-Goalkeeper, inherits from Player (other specific fields related to this role)
I think that Single Table Inheritance is a bad solution, because there will be a lot of null fields in the table created. What is the best way to do this? Use polymorphic associations (with has_one?)? Use belongs_to/has_one (but then how to show in the Player views the fields of Person too?)? Don't implement inheritance? Other solutions?

While I think STI is probably the approach I would use for this, one other possibility, if you want to avoid a lot of NULL attributes, is to add a column other_attributes to your Person model that will store a Hash of attributes. To do this, add a text column to the people table:
def self.up
add_column :people, :other_attributes, :text
end
Then make sure the attribute is serialized in the model. And you may want to write a wrapper to make sure it's initialized as an empty Hash when you use it:
class Person < ActiveRecord::Base
serialize :other_attributes
...
def other_attributes
write_attribute(:other_attributes, {}) unless read_attribute(:other_attributes)
read_attribute(:other_attributes)
end
end
Then you can use the attribute as follows:
p = Person.new(...)
p.other_attributes #=> {}
pl = Player.new(...)
pl.other_attributes["position"] = "forward"
pl.other_attributes #=> {"position" => "forward"}
One caveat with this approach is that you should use strings as keys when retrieving data from other_attributes, as the keys will always be strings when the Hash is retrieved from the database.

I suggest STI. An alternative solution is to use a document store like mongodb, or use the activerecord store http://api.rubyonrails.org/classes/ActiveRecord/Store.html. If you have a postgress database look at his HStore column http://rubygems.org/gems/activerecord-postgres-hstore.
Another option is PostgreSQL table inheritance. http://www.postgresql.org/docs/8.1/static/ddl-inherit.html

Related

Single Table inheritance with CRUD and forms in Rails

I'm a bit confused about STI in rails.
My situation:
I have a Contact model that has description and data string fields, to store some contact like phone, fax, email, etc.
Now when I have some specific contact type like phone number of email address I want to walidate the data format in different way and I want to make some different formating on output.
I decided to use STI as all the models have the same data with just different behaviour. And I have some questions regarding forms and CRUD operations as I don't want to go against Rails conventions.
How do I make a dropdown list in form with model type? Should I hardcode it or is there some more automated way?
How do I create a record? Should I use switch statement and according to received type create new model of according instance?
How should I update it if I'm going to change the model type? Cast the object to new class? Or create a new object and destroy the previous one?
I'll be very thankfull for your help!
Yes, should do a hardcore as there no default store for your STI models.
Generally, yes. But With Rails you could just use camelize.constantize to get class from string. Another way is just use parent model, and set type field manually. As with STI all records are in the same table and then all are of the parent class.
If you wish to update, just update type field. Then you could re-query to force Rails to get new object of different type.
You could create a model like this :
Type < ActiveRecord::Base
has_many :contacts
end
You could use this command rails g model Type name:string, add a type_id column in your contact and migrate the database.
end change your contact's model like this :
Contact < ActiveRecord::Base
belongs_to :type
end
Now, in your form, you could use this :
select("type", "type_id", Type.all.collect {|t| [ t.name, t.id ] }, { :include_blank => true })
It should resolve your problem.
Now you can do something like this :
#emails = Type.find_by_name('email').contacts
Or use scopes.

Using existing tables and its relations in rails 3.2 application?

I have an existing application with lot of data and about 20 tables, how can I use them directly.
My database.yml file points to the MysQL database. Something like magic model generator.
you can do the following
connection = ActiveRecord::Base.connection()
results = connection.execute("#{your_sql_query_here}")
results.each do |row|
puts row[0]
end
However I'd recommend you to associate them in a more coherent fashion.
You would create a model for each one of them which is not much when you only have about 20.
In the model you would associate the table to them using the
set_table_name :name_of_your_table
Keep in mind the model name just have to be relavent enough to the table, because of the explicit set_table_name method they do not have to follow strict convention.
And to set relationships you would use the class_name just like this
has_many :fruits, :class_name => "CrazyFruit"
It might sound tedious but its the right way

how to force a has_many to use a specific :foreign_key?

I'm having trouble with a model not honoring the :foreign_key policy.
Character model has the following fields:
name:string
level:int
realm:string
realm_id:integer
class Character < ActiveRecord::Base
belongs_to :realm
end
My Realms model looks like this:
class Realm < ActiveRecord::Base
has_many :characters, :foreign_key => "realm_id"
end
However, it seems like it's forcing the character model to use the :realm column as the foreign_key rather than :realm_id. I don't have any clue as to why or how to fix it. Is there any other way to make it ignore the :realm field and go for the :realm_id without having to change the name of the column?
[Edit for clarity]
The character model does have a realm_id:integer field. I have tried not having the foreign_key but the results with both is identical.
ruby-1.9.2-p136 :012 > c = Character.new
=> #
ruby-1.9.2-p136 :013 > c.realm = "Sargeras"
ActiveRecord::AssociationTypeMismatch: Realm(#2154038240) expected, got String(#2151988680)
Despite even having the foreign_key, it just refuses to let go of the realm column.
[Edit 2]
The realm column will just take over due to the has_many and belongs_to association. There is no way so far to break this, so the solution is to either remove the column (the approach i will take), or rename it to something different.
Did you make sure that your Character table has a realm_id column? Please make sure of that, and then get rid of foreign_key => 'realm_id, it is not necessary at all. Your program should work if you get both these things done.
You should not need the :foreign_key part here at all, since you're following the standard Rails naming convention, the realm_id column should be inferred from the model name.
EDIT
I see. I don't think you can have a column and an association by the same name in one model. The easiest solution would probably be to rename the "realm" column to "realmname" or something like that.
belongs_to :realm creates (among other things) methods called realm and realm= used as getters and setters. That means the method_missing magic that ActiveRecord uses to expose database columns as attributes is never triggered when you do Character#realm, as that method isn't in fact missing.
If you want to avoid renaming the realm column in your database, you could create attribute accessors for the field manually under another name:
class Character < ActiveRecord::Base
def realm_name
self['realm']
end
def realm_name=(value)
self['realm'] = value
end
end
This way you'll still have the realm column in your database and be able to access the attribute in Ruby, albeit under a different name. This isn't a great idea, though, as you'll be duplicating the realm name as both Character#realm_name and Character.realm.name.
I would ditch the realm column and instead make sure I use Realm objects when importing from the datasource:
character.realm = Realm.find_by_name('Sargeras')
That way you'd only realm data available where it makes sense; in the Realm model.

Model associations

I have two models Library and Book. In my Library model, I have an array - book_ids. The primary key of Book model is ID.
How do I create a has_many :books relation in my library model?
This is a legacy database we are using with rails.
Thanks.
Your database schema doesn't really conform with the prescribed Rails conventions so you will probably have a hard time making the default has_many association work. Have you tried fiddling with the custom SQL options with it thought?
If you can't get the built in has_many association to work, you'll have to roll your own. I would define the books and books= methods on your Library model, and inside them set a virtual attribute, which you then save as an array in the database. Perhaps something like this:
class Book > ActiveRecord::Base; end
class Library > ActiveRecord::Base
before_save :serialize_books
def books
#books || nil
end
def books=(new_books)
#books = new_books
end
private
def serialize_books
#attributes['books'] = "[" + #books.collect {|b| b.id }.join(',') + "]"
end
end
That up there wouldn't pull out the dataIf you wanted to go even more gung ho and support single query find operations, you could use some custom SQL in a scope or override find and add it to the default options. Comment if you want help with any of this!
If you want to use has_many you could use the options :counter_sql and :finder_sql using the MySQL LIKE or REGEX syntax. But its probably better to first load the Libary model, then parse the book_ids column and load the books, or directly build a query with that string.
Consider using :serialize method with ActiveRecord:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002284
it might do what you want

Pattern for unidirectional has_many join?

It occurred to me that if I have a has_many join, where the foreign model does not have a belongs_to, and so the join is one way, then I don't actually need a foreign key.
We could have a column, category_ids, which stores a marshaled Array of IDs which we can pass to find.
So here is an untested example:
class page < AR
def categories
Category.find(self.category_ids)
end
def categories<<(category)
# get id and append to category_ids
save!
end
def category_ids
#cat_ids ||= Marshal.load(read_attribute(:category_ids)) rescue []
end
def category_ids=(ids)
#cat_ids = ids
write_attribute(:category_ids, ids)
end
end
page.category_ids => [1,4,12,3]
page.categories => Array of Category
Is there accepted pattern for this already? Is it common or just not worth the effort?
Wouldn't performance suffer here when you are marshalling / unmarshalling?
I personally don't think this is worth the effort and what you are trying to do doesn't seem clear.
Actually this looks like a many-to-many mapping rather than a many-to-one, as there is no code that prevents a category from belonging to more than one page, surely you want something like:
create table categories_pages (
category_id integer not null references categories(id),
page_id integer not null references pages(id),
primary_key(category_id, page_id)
);
with either a has and belongs to many on both sides or has_many :through on both sides (depending on whether you want to store more stuff).
I agree with Omar that this doesn't seem to be worth the effort.
Your database no longer reflects your data model and isn't going to provide any help in enforcing this relationship. Also, you now have to keep your marshalled id array in sync with the Category table and enforce uniqueness across Pages if you want to restrict the relationship to has_many.
But I guess most importantly, what is the benefit of this approach? It is going to increase complexity and increase the amount of code you have to write.

Resources