Only one side of join table being saved? - ruby-on-rails-3

In my app, User objects can follow each other, and be followed. The two relationships are distinct.
I'm seeing that when I set user_a.follows << user_b that user_b.followed_by.count still == 0. Why?
When I play in the console, I see:
$ jordan = User.new(:name=>"Jordan")
=> #<User id: nil, name: "Jordan">
$ matt = User.new(:name=>"Matt")
=> #<User id: nil, name: "Matt">
$ matt.followers << jordan
=> [#<User id: nil, name: "Jordan">]
$ matt.followers.first
=> #<User id: nil, name: "Jordan">
$ jordan.friends.first
=> nil
$ matt.save
SQL (14.1ms) INSERT INTO "users" ("name") VALUES (?) [["name", "Matt"]]
SQL (0.3ms) INSERT INTO "users" ("name") VALUES (?) [["name", "Jordan"]]
SQL (0.4ms) INSERT INTO "followings" ("followee_id", "follower_id") VALUES (?, ?) [["followee_id", nil], ["follower_id", 2]]
=> true
My objects are defined as:
class User < ActiveRecord::Base
has_many :follower_followee_rel,
:class_name => "Following",
:foreign_key => 'followee_id',
:dependent => :destroy
has_many :friends,
:through => :follower_followee_rel,
:source => :followee
has_many :followee_follower_rel,
:class_name => 'Following',
:foreign_key => 'follower_id',
:dependent => :destroy
has_many :followers,
:through => :followee_follower_rel,
:source => :follower
end
class Following < ActiveRecord::Base
belongs_to :followee,
:class_name => 'User'
belongs_to :follower,
:class_name => 'User'
end
Totally ignoring the second half of the relationship.
No errors are raised. What's going on?

Join relationships don't work unless the two models that are being joined have both already been saved. You can see on the 3rd line of the SQL that nil is being inserted for followee_id, as jordan doesn't yet have an id.
You would also need to save matt before you check jordan's friends, because doing matt.followers << jordan is treated as a modification of matt that doesn't effect anything else until matt is saved.

Implementing Michael Fairley's suggestion got ActiveRecord to build the relationship, but the relationship was still being defined wrong.
Final solution was to save the records in advance of joining them, and to fix the foreign keys, which were backwards.
Was:
has_many :follower_followee_rel,
:class_name => 'Following',
:foreign_key => 'followee_id',
:dependent => :destroy
has_many :friends,
:through => :follower_followee_rel,
:source => :followee
has_many :followee_follower_rel,
:class_name => 'Following',
:foreign_key => 'follower_id',
:dependent => :destroy
has_many :followers,
:through => :followee_follower_rel,
:source => :follower
Is now:
has_many :follower_followee_rel,
:class_name => 'Following',
:foreign_key => 'follower_id',
:dependent => :destroy
has_many :friends,
:through => :follower_followee_rel,
:source => :followee
has_many :followee_follower_rel,
:class_name => 'Following',
:foreign_key => 'followee_id',
:dependent => :destroy
has_many :followers,
:through => :followee_follower_rel,
:source => :follower
Change is found #
:follower_followee_rel, :foreign_key=>'followee_id'
and
:followee_follower_rel, :foreign_key=>'followee_id'

Related

Friendship has_many through model with multiple status'

Currently my User model has the following code:
has_many :notifications
has_many :friendships
has_many :friends, :through => :friendships, :conditions => { status: 'accepted' }
has_many :requested_friends, :through => :friendships, :source => :friend, :conditions => { status: 'requested' }
has_many :pending_friends, :through => :friendships, :source => :friend, :conditions => { status: 'pending' }
And my Friendship model is as follows:
belongs_to :user
belongs_to :friend, :class_name => "User"
def self.request(user, friend)
unless user == friend or Friendship.exists?(user_id: user, friend_id: friend)
transaction do
Friendship.create(:user => user, :friend => friend, :status => 'pending')
Friendship.create(:user => friend, :friend => user, :status => 'requested')
end
else
return "failed"
end
end
def self.accept(user, friend)
unless user == friend or Friendship.exists?(user_id: user, friend_id: friend)
transaction do
accepted_at = Time.now
accept_one_side(user, friend, accepted_at)
accept_one_side(friend, user, accepted_at)
end
else
return "failed"
end
end
def self.accept_one_side(user, friend, accepted_at)
request = find_by_user_id_and_friend_id(user, friend)
request.status = 'accepted'
request.accepted_at = accepted_at
request.save!
end
When I try to run my cucumber tests, however, I am getting this error:
SQLite3::SQLException: no such column: users.status: SELECT "users".* FROM "users" INNER JOIN "friendships" ON "users"."id" = "friendships"."friend_id" WHERE "users"."status" = 'accepted' AND "friendships"."user_id" = ? (ActionView::Template::Error)
I think that this means it is trying to only include inside in for example pending_friends, users which have the attribute status = "pending", where it should actually be including users who belong to friendships which have attribute status = "pending"
Is this right? How would I go about fixing this?
I can't find any documentation on use of :conditions with has_many, but based on the error that's being generated, I gather that the conditions specified are assumed to apply to the model that is subject of the has_many, not the target model or the model through which it is being reference.
I have updated to the following and this works:
has_many :notifications
has_many :friendships
has_many :accepted_friendships, :class_name => "Friendship", :conditions => {status: 'accepted'}
has_many :requested_friendships, :class_name => "Friendship", :conditions => {status: 'requested'}
has_many :pending_friendships, :class_name => "Friendship", :conditions => {status: 'pending'}
has_many :friends, :through => :accepted_friendships
has_many :requested_friends, :through => :requested_friendships, :source => :friend
has_many :pending_friends, :through => :pending_friendships, :source => :friend
If anyone has a different approach without having to create accepted_friendshis, requested_friendships, and pending_friendships, however, I would love to hear it!
status is a column for friendships table.
So when you are writing the code, then mention the table name or else it will take the table of the Current model.
has_many :friends, -> { where "friendships.status = 'accepted'" }, :through => :friendships

How should I model horse pedigrees in rails?

I need to model up to 5 or 6 generations horse pedigrees using rails/activerecord. I did my research here on stack and on the web and ultimately utilized this article as the basis of my approach. Here's what I've come up with.
Two models:
Horse has the following attributes id and horse_name
Pedigree has: id, parent_id and horse_id.
And the following associations:
has_many :parent_horse_relationships, :class_name => "Pedigree", :foreign_key => :horse_id, :dependent => :destroy
has_one :sire_horse_relationship, :class_name => "Pedigree", :foreign_key => :horse_id, :conditions => "horse_gender = 'Male'
has_one :dam_horse_relationship, :class_name => "Pedigree", :foreign_key => :horse_id, :conditions => "horse_gender = 'Female'
has_many :parents, :through => :parent_horse_relationships, :source => :parent
has_one :sire, :through => :sire_horse_relationship,:source => :parent
has_one :dam, :through => :dam_horse_relationship,:source => :parent
has_many :horse_parent_relationships, :class_name => "Pedigree", :foreign_key => :parent_id, :dependent => :destroy
has_many :progenies, :through => :horse_parent_relationships, :source => :horse
This approach is close, however it appears my condition to determine the dam or sire is being applied to the Horse and not the parent. Therefore if the particular horse is Male, the horse.sire will work, but the horse.dam will not and vice versa. Once I get basic functionality working I'd like to add additional methods to get the whole pedigree, grandparents, siblings, descendants, etc.
Questions:
How can I apply the gender condition to the parents and not the horse so that both sire and dam work.
Is the approach that I have take viable or is there a more elegant, efficient way of accomplishing this.
Any other suggestions or guidance would be appreciated.
Apologies for the long question and thanks for your help.
I might start with:
has_one :sire, :class_name => "Pedigree", :foreign_key => :horse_id, :conditions => "horse_gender = 'Male'
has_one :dam, :class_name => "Pedigree", :foreign_key => :horse_id, :conditions => "horse_gender = 'Female'
has_many :parent_horse_relationships, :class_name => "Pedigree", :foreign_key => :horse_id, :dependent => :destroy
has_many :parents, :through => :parent_horse_relationships, :source => :parent
has_many :progenies, :through => :horse_parent_relationships, :source => :horse
I ended up spending a great deal of time on this one, but finally came up with a solution that met my requirements. The associations that ultimately worked follow:
has_many :parent_horse_relationships, :class_name => "Pedigree", :foreign_key => :horse_id, :dependent => :destroy
has_many :parents, :through => :parent_horse_relationships, :source => :parent do
def dam_relationship
owner = self.proxy_association.owner
owner = owner.parents.where(:horse_gender => "Female")
where('pedigrees.parent_id = ?', owner)
end
def sire_relationship
owner = self.proxy_association.owner
owner = owner.parents.where(:horse_gender => "Male")
where('pedigrees.parent_id = ?', owner)
end
end
def dam
parents.dam_relationship
end
def sire
parents.sire_relationship
end
Question responses:
I applied the gender condition through use of an association_proxy and a simple wrapper. I created a dam_relationship and corresponding sire_relationship and then wrapped those methods in a couple of dam and sire wrapper methods.
def dam_relationship
owner = self.proxy_association.owner
owner = owner.parents.where(:horse_gender => "Female")
where('pedigrees.parent_id = ?', owner)
end
def dam
parents.dam_relationship
end
This allows me to do:
#horse.parents, #horse.dam, #horse.sire (not displayed)
as well as most of the methods included in the ancestry gem mentioned below. With a little bit of recursion it's fairly straight forward to display the entire pedigree or the number of generations that interest you.
I decided that the approach of having two models (Horse and Pedigree) provide som additional flexibility compared to having the sire_id and dam_id directly in the Horse model. This approach will enable me to more easily create methods like #horse.uncle, #horse.aunt. I believe these would be more difficult with the sire_id and dam_id directly in the Horse model.
The most popular gem for accomplishing this seems to be ancestry. The author accomplishes this and a lot more simply by adding an ancestry column to the model of interest. Its a very nice solution a definitely worth checking out.

Multiple has_many :through

In object model, I have
has_many :likes
has_many :hates
has_many :users, :through => :likes
has_many :users, :through => :hates
How do I get the list of users for likes? E.g. object.users <--- but how do I specify through likes or hates?
You need to give these two different associations different names. What about
has_many :likes
has_many :hates
has_many :likers, :through => :likes, :source => :user
has_many :haters, :through => :hates, :source => :user
It seems that I need to add source too. If not Rails will be looking for likers/liker in likes.
has_many :likes
has_many :hates
has_many :likers, :through => :likes, :class_name => 'User', :source => 'user'
has_many :haters, :through => :hates, :class_name => 'User', :source => 'user'
You can do it like :
has_many :user_likes, :through => :likes, :class_name => 'User'

RSpec testing non validating has_many through relationship

I have a many to many relationship between documents.
Say I have document1 and document2. I have a many to many table where there are parents and children.
document.rb
has_many :child_relationships, :class_name => "DocumentRelationship", :foreign_key => "child_id", :dependent => :destroy
has_many :parents, :through => :child_relationships, :source => :parent
has_many :parent_relationships, :class_name => "DocumentRelationship", :foreign_key => "parent_id", :dependent => :destroy
has_many :children, :through => :parent_relationships, :source => :child
document_relationship.rb
belongs_to :parent, :class_name => "Document", :foreign_key => "parent_id"
belongs_to :child, :class_name => "Document", :foreign_key => "child_id"
validates_uniqueness_of :child_id, :scope => [:parent_id]
validates_presence_of :parent_id
validates_presence_of :child_id
validate :obeys_chronology
def obeys_chronology
errors.add(:child_id, "must be created after its parent") if child_id.to_i < parent_id.to_i
errors.add(:child_id, "cannot be its own parent") if child_id.to_i == parent_id.to_i
end
If I say document2.children << document1 it appropriately fails validation, but I don't know how to write a test for this.
Is there a better way to do this?
Add it to the collection
document2.children << document1
document2.children.contain?(document1).should == false
Then make sure it's not in there.

How to write an AREL statement on a self-referencing table

I have written quite a few AREL statements, but I'm tying myself in knots over this one. Here is my situation:
class Product < AR::Base
has_many :parents, :class_name => "ProductLink", :foreign_key => :to_product_id
has_many :children, :class_name => "ProductLink", :foreign_key => :from_product_id
# has an attribute called "identifier"
end
class ProductLink < AR::Base
belongs_to :parent, :class_name => "Product", :foreign_key => :from_product_id
belongs_to :child, :class_name => "Product", :foreign_key => :to_product_id
end
I want to retrieve all of the Products that have a child product with an identifier that matches some value.
I have twisted myself into a pretzel with this, seems easy, but I have been looking at it for too long now. I appreciate any help!
Got it!
brand.products.joins(:children => :child).where(:children => { :child => { :searchable_identifier.matches => "2136" } } )
That works great. See the hashed joins? That's what was throwing me off.