Pattern for unidirectional has_many join? - ruby-on-rails

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.

Related

Query model data with relationship count and performance

I have problems with query performance with Postgresql and Rails counting related models while retrieving data.
class MasterModel
# few fields, like name, description and such
has_and_belongs_to_many :business_models, class: 'BusinessModel'
end
class BusinessModel
# Lots of important information, many fields
has_and_belongs_to_many :master_models, class: 'MasterModel'
end
The use case in question being that business_model can be related to any amount of master_model so typically you should have a small amount of master_model, a great amount of business_model and a even bigger amount of many to many relationships.
When showing master_model index page, you can visualize its information and a delete button only enabled when there are no relationships, hence the reason why its important to count the relationship in its representation.
So I tried some ways to achieve this:
Includes relationship is incredibly slow in ActiveRecord but not in query time. At least it has no N+1.
MasterModel.includes(:business_models).limit(50).offset(0).each do |master|
master.business_models.size
end
No includes. We have N+1 but is incredibly fast as long as model pagination is reasonable.
MasterModel.limit(50).offset(0).each do |master|
master.business_models.size
end
Given that I only need to know if relationships exists or not I tried a select with exists. Single query and fast.
MasterModel.select(
:id,
:name,
:description.
'NOT EXISTS (
SELECT many.master_id
FROM many
WHERE many.master_id = master.id
) AS removable'
).limit(50).offset(0).each do |master|
master.business_models.removable
end
In the end, I chose the 3rd choice but I am not totally convinced. What would be the Rails way? Am I doing something wrong in the other cases?
If you would used has_many through association you would be able to use counter_cache but HABTM doesn't support counter_cache so that you can implement your own counter_cache
First of all you need to add new integer column to the master_models table called business_models_count
add_column :master_models, :business_models_count, :integer, default: 0
And add next code to your model MasterModel
class MasterModel
has_and_belongs_to_many :business_models, class: 'BusinessModel', before_add: :inc_business_models_count, before_remove: :dec_business_models_count
private
def inc_business_models_count(*)
self.increment!(:business_models_count)
end
def dec_business_models_count(*)
self.decrement!(:business_models_count)
end
end
And write some rake task which goes through MasterModel records and update counter for existing records.
It can be done like this:
MasterModel.find_each do |master|
master.increment!(:business_models_count, master.business_models.size)
end
And after that you will be able to get business_models_count of each MasterModel instance without N+1
MasterModel.limit(50).offset(0).each do |master|
master.business_models_count
end

Ruby on Rails - Counting goals of a team in many matches

I've got a Match model and a Team model.
I want to count how many goals a Team scores during the league (so I have to sum all the scores of that team, in both home_matches and away_matches).
How can I do that? What columns should I put into the matches and teams database tables?
I'd assume your Match model looks something like this:
belongs_to :home_team, class_name:"Team"
belongs_to :away_team, class_name:"Team"
attr_accessible :home_goal_count, :away_goal_count
If so, you could add a method to extract the number of goals:
def goal_count
home_matches.sum(:home_goal_count) + away_matches.sum(:away_goal_count)
end
Since this could be expensive (especially if you do it often), you might just cache this value into the team model and use an after_save hook on the Match model (and, if matches ever get deleted, then an after_destroy hook as well):
after_save :update_team_goals
def update_team_goals
home_team.update_attribute(:goal_count_cache, home_team.goal_count)
away_team.update_attribute(:goal_count_cache, away_team.goal_count)
end
Since you want to do this for leagues, you probably want to add a belongs_to :league on the Match model, a league parameter to the goal_count method (and its query), and a goal_count_cache_league column if you want to cache the value (only cache the most recently changed with my suggested implementation, but tweak as needed).
You dont put that in any table. Theres a rule for databases: Dont ever store data in your database that could be calculated from other fields.
You can calcuate that easyly using this function:
def total_goals
self.home_matches.collect(&:home_goals).inject(&:+)+self.away_matches.collect(&:away_goals).inject(&:+)
end
that should do it for you. If you want the mathes filtered for a league you can use a scope for that.

How to write a method for all Classes

I am trying to write a method that would apply directly to several models with HABTM relations to clean up any unused relations.
def cleanup
self.all.each do |f|
if f.videos.count == 0
self.destroy(f)
end
end
end
Where do I save this method to and is this even the correct syntax for such a method? It would theoretically be run as:
>>Tag.cleanup
Write external module and include it in each Model you need
Sadly people keep on using has_and_belongs_to_many even though it leads to all kinds of orphans like this. A has_many ..., :through relationship can be flagged :dependent => :destroy to clean up unused children automatically. It's common that you'll have unused join records and they are obnoxious to remove.
What you might do is approach this from a SQL angle since has_and_belongs_to_many records are inaccessible if their parent records are no longer defined. They simply do not exist as far as ActiveRecord is concerned. Using a join model means you can always access this data since they are issued their own ids.
has_and_belongs_to_many relationships are based on a compound key which makes removing them a serious nuisance. Normally you'd do a DELETE FROM table WHERE id IN (...) AND ... and be confident that only the target records are removed. With a compound key you can't do this.
You may find this works for an example Tag to Item relationship:
DELETE FROM item_tags, tags, items WHERE item_tags.tag_id=tags.id AND item_tags.item_id=items.id AND tags.id IS NULL AND items.id IS NULL
The DELETE statement can be really particular about how it operates and does not give the same latitude as a SELECT with joins that can be defined as left or right, inner or outer as required.
If you had a primary ID key in your join table you could do it easily:
DELETE FROM item_tags WHERE id IN (SELECT id FROM item_tags LEFT JOIN tags ON item_tags.tag_id=tags.id LEFT JOIN items ON item_tags.item_id=items.id WHERE tags.id IS NULL AND items.id IS NULL)
In fact, it might be advantageous to add a primary key to your relationship table even if ActiveRecord ignores it.
Edit:
As for your module issue, if you're stuck with that approach:
module CleanupMethods
def cleanup
# ...
end
end
class Tag
# Import the module methods as class methods
extend CleanupMethods
end
If you use a counter cache column you can do this a lot more easily, but you will also have to ensure your counter caches are accurate.
You want to add a class method to the Tag class, and instead of iterating through all the tag objects (requiring rails to load each one) and then checking for videos through Active Record, it's faster to load all the orphaned records using a query and then destroy only those.
Guessing that you have tags and videos, here, and that tag_videos is your join table, in Rails 2.x you might write
def self.cleanup
find(:all, :conditions => "id NOT IN (select tag_id from tag_videos)").destroy_all
end
In Rails 3 you'd write
def self.cleanup
where("id NOT IN (select tag_id from tag_videos)").destroy_all
end

Access join table data in rails :through associations

I have three tables/models. User, Alliance and Alliance_Membership. The latter is a join table describing the :Alliance has_many :Users through :Alliance_Membership relationship. (:user has one :alliance)
Everything works ok, but Alliance_Membership now has an extra field called 'rank'. I was thinking of the best way to access this little piece of information (the rank).
It seems that when i do "alliance.users", where alliance is the user's current alliance object, i get all the users information, but i do not get the rank as well. I only get the attributes of the user model. Now, i can create a helper or function like getUserRole to do this for me based on the user, but i feel that there is a better way that better works with the Active Record associations. Is there really a better way ?
Thanx for reading :)
Your associations are all wrong - they shouldn't have capital letters. These are the rules, as seen in my other answer where i told you how to set this up yesterday :)
Class names: Always camelcase like AllianceMembership (NOT Alliance_Membership!)
table names, variable names, methods and associations: always underscored and lower case:
has_many :users, :through => :alliance_memberships
To find the rank for a given user of a given alliance (held in #alliance and #user), do
#membership = #alliance.alliance_memberships.find_by_user_id(#user.id)
You could indeed wrap this in a method of alliance:
def rank_for_user(user)
self.alliance_memberships.find_by_user_id(user.id).rank
end

Reverse Polymorphic Associations

I have a parent object, Post, which has the following children.
has_one :link
has_one :picture
has_one :code
These children are mutually exclusive.
Is there a way to use polymorphic associations in reverse so that I don't have to have link_id, picture_id, and code_id fields in my Post table?
I wrote up a small Gist showing how to do this:
https://gist.github.com/1242485
I believe you are looking for the :as option for has_one. It allows you to specify the name of the belongs_to association end.
When all else fails, read the docs: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_one
Is there a way to use polymorphic
associations in reverse so that I
don't have to have link_id,
picture_id, and code_id fields in my
Post table?
has_one implies that the foreign key is in the other table. If you've really defined your model this way, then you won't have link_id, picture_id, and code_id in your Post table. I think you meant to say belongs_to.
I want to do something like
#post.postable and get the child
object, which would be one of link,
picture, or code.
I believe you could do this by using STI and combining the links, pictures, and codes tables, then testing the type of the model when retrieving. That seems kludgey though, and could end up with lots of unused columns.
Is there a reason for not storing the unused id columns, other than saving space? If you're willing to keep them, then you could define a virtual attribute and a postable_type column : (untested code, may fail spectacularly)
def postable
self.send(self.postable_type)
end
def postable=(p)
self.send(postable_type.to_s+"=",p)
end

Resources