Created
June 19, 2014 06:17
-
-
Save shoaibmalik786/bd7e06c62449a4dd1142 to your computer and use it in GitHub Desktop.
Self refrencial associaion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Let’s begin with some basic assumptions: we have a model class called User and we want the User to know three things: | |
| a list of users directly linked to (me → someone) | |
| a list of users directly linked from (someone → me) | |
| a list of users directly linked, no matter to or from (e.g. if A → B and B → C, then A and C should be on B‘s linked list) | |
| As we can see, the relationship between two users is directed: one user initiated it and the other responded. But when looking for a path between two ‘distant’ users, we don’t care who initiated an individual relationship that could be part of the path, so we also need an undirected view on relationships. | |
| I left out the confirmation feature, as this is easy and not particularly interesting to implement. | |
| Database | |
| For starters, we need two tables in the database, users and relationships. Here are excerpts from my migration files, stripped of anything unnecessary. | |
| create_table :users do |t| | |
| t.column :login, :string, :null => false | |
| end | |
| create_table :relationships, :id => false do |t| | |
| t.column "user_id", :integer, :null => false | |
| t.column "buddy_id", :integer, :null => false | |
| end | |
| There is nothing special in these tables, I think that they would look like that even if we used anything other than Rails. The users table should of course contain additional user attributes, and relationships is a typical join table and could store information whether the link is confirmed, creation and confirmation dates and so on. Most of you would also probably use foreign keys constraints to have the database enforce the data integrity (omited here for simplicity). | |
| Specifications | |
| Next, let’s describe how the relationships on User should work. I use rspec, which combined with autotest, turns BDD into a pleasure rather than work. Here’s user_spec.rb: | |
| describe User do | |
| before :each do | |
| %w(u v w).each do |x| | |
| eval "@#{x} = User.new; @#{x}.login = '#{x}'; @#{x}.save" | |
| end | |
| end | |
| it "can be linked with another user" do | |
| @u.linked_to << @v | |
| @u.linked_to.should include(@v) | |
| u2 = User.find_by_id @u.id | |
| u2.linked_to.should include(@v) | |
| end | |
| it "can find users that linked to it" do | |
| @u.linked_to << @v | |
| @v.linked_from.should include(@u) | |
| end | |
| it "can see friends of its friends" do | |
| @u.linked_to << @v | |
| @v.linked_to << @w | |
| @u.linked_to[0].linked_to.should include(@w) | |
| @w.linked_from[0].linked_from.should include(@u) | |
| end | |
| it "has a list of all users linked to and from it" do | |
| @u.linked_to << @v | |
| @v.linked_to << @w | |
| @v.linked.should include(@u) | |
| @v.linked.should include(@w) | |
| end | |
| end | |
| The before method defines three instance variables @u, @v and @w. Rails requires the objects in many-to-many associations to be already saved to the database, so we give them appropriate login names and save them. I use the each loop combined with eval so I don’t have to repeat the almost-the-same code three times. | |
| All the other ‘methods’ are pretty self-explanatory, as always when using rspec. I assumed that User has thee collections: linked_to, linked_from and linked, which implement the above mentioned lists. Then I test some basic relations between those collections. For example, if user @u links to @v, then @v should have @u on its linked_from list. | |
| Models | |
| Having specified User's behavior, we can try to implement it along with helping class, Relationship: | |
| class Relationship < ActiveRecord::Base | |
| belongs_to :user, | |
| :class_name => 'User', :foreign_key => 'user_id' | |
| belongs_to :buddy, | |
| :class_name => 'User', :foreign_key => 'buddy_id' | |
| end | |
| class User < ActiveRecord::Base | |
| has_many :relations_to, | |
| :foreign_key => 'user_id', :class_name => 'Relationship' | |
| has_many :relations_from, | |
| :foreign_key => 'buddy_id', :class_name => 'Relationship' | |
| has_many :linked_to, | |
| :through => :relations_to, :source => :buddy | |
| has_many :linked_from, | |
| :through => :relations_from, :source => :user | |
| end | |
| Class Relationship has no particular business purpose (for now), but defines the roles: the initiator of the link is called user, and buddy is the responding party. | |
| Then comes the User class, full of ActiveRecord magic that I, to be frank, don’t fully understand. It looks nice and symmetrical, but took many tries and some googling to get it to work. Probably this could be simplified, but I’m happy that it works. | |
| The linked list | |
| OK, we have linked_to and linked_from, but where is linked list?! — I hear you exclaim. Well, currently I don’t know how to implement it using pure Rails magic, so I resorted to a little bit more of SQL. I create a view called buddies: | |
| create or replace view buddies as | |
| select user_id, buddy_id | |
| from relationships | |
| union | |
| select buddy_id, user_id | |
| from relationships | |
| Here I use a little trick: the view lists the contents of relationships table two times — first normally, and then reversed. So, if relationships contains a tuple (1, 2), then the view has both (1, 2) and (2, 1). | |
| Having this view in place, we can add following method to User class: | |
| def linked | |
| User.find_by_sql " | |
| select * | |
| from users | |
| where id in ( | |
| select buddy_id | |
| from buddies | |
| where user_id = #{id})" | |
| end | |
| The inner select finds id’s of all the users that are either linked to or linked from the current one, and the outer select just reads the user data. This is probably far from being optimal and should be done without resorting to find_by_sql, but it works. | |
| Obviously, the collection returned by linked method doesn’t support adding and removing buddies, and that’s a good thing, because we need to know who initiated the link. To have the relations changed, we should use operations provided by linked_to and linked_from. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment