ActiveRecord Association without Foreign Key - ruby-on-rails

I am trying to build a relationship model between users. A user can either initiate a relation, or receive a relation from another user. Therefore, the relations table in the db has the foreign keys "initiator_id" and "recipient_id".
Now, I can figure what relations the user initiated or received using the following associations:
has_many :initiated_relations, :foreign_key => :initiator_id, :class_name => 'Relation', :dependent => :destroy
has_many :received_relations, :foreign_key => :recipient_id, :class_name => 'Relation', :dependent => :destroy
What I am trying to do, is build an association that will fetch me all relations that belong to a user (either initiated or received). Trying the following does not work, and complains about the lack of "user_id" field:
has_many :relations, :conditions => 'recipient_id = #{id} or initiator_id = #{id}'
How can I create an association that is solely based on the conditions field, without looking for the default foreign_key? Or is there perhaps a completely different approach to solving this?

From your comments to #neutrino's answer I understand, that you only need this "relation" for read only operations. If you're on Rails 3 you can utilize the fact, that it uses lazy fetching. The where() method returns ActiveRecord::Relation object, which you can later modify. So you can define a method like this:
def User < ActiveRecord::Base
def all_relations
Relation.where("initiator_id => ? OR recipient_id = ?", id, id)
end
end
And then you can do:
User.all_relations.where(:confirmed => true).all

Well, I can think of using finder_sql for that:
has_many :relations, :finder_sql => 'select * from relations right outer join users
on relations.recipient_id = #{id} or relations.initiator_id = #{id}'
Apart from that, you can just write a method that will return a united array of the two relations associations', but you will lose the advantage of an association interface (phew).
Perhaps someone will come up with a better solution.

Related

Rails ActiveRecord how to order by a custom named association

Ok so have created 2 models User and Following. Where User has a username attribute and Following has 2 attributes which are User associations: user_id, following_user_id. I have set up these associations in the respective models and all works good.
class User < ActiveRecord::Base
has_many :followings, dependent: :destroy
has_many :followers, :class_name => 'Following', :foreign_key => 'following_user_id', dependent: :destroy
end
class Following < ActiveRecord::Base
belongs_to :user
belongs_to :following_user, :class_name => 'User', :foreign_key => 'following_user_id'
end
Now I need to order the results when doing an ActiveRecord query by the username. I can achieve this easily for the straight-up User association (user_id) with the following code which will return to me a list of Followings ordered by the username of the association belonging to user_id:
Following.where(:user_id => 47).includes(:user).order("users.username ASC")
The problem is I cannot achieve the same result for ordering by the other association (following_user_id). I have added the association to the .includes call but i get an error because active record is looking for the association on a table titled following_users
Following.where(:user_id => 47).includes(:user => :followers).order("following_users.username ASC")
I have tried changing the association name in the .order call to names I set up in the user model as followers, followings but none work, it still is looking for a table with those titles. I have also tried user.username, but this will order based off the other association such as in the first example.
How can I order ActiveRecord results by following_user.username?
That is because there is no following_users table in your SQL query.
You will need to manually join it like so:
Following.
joins("
INNER JOIN users AS following_users ON
following_users.id = followings.following_user_id
").
where(user_id: 47). # use "followings.user_id" if necessary
includes(user: :followers).
order("following_users.username ASC")
To fetch Following rows that don't have a following_user_id, simply use an OUTER JOIN.
Alternatively, you can do this in Ruby rather than SQL, if you can afford the speed and memory cost:
Following.
where(user_id: 47). # use "followings.user_id" if necessary
includes(:following_user, {user: :followers}).
sort_by{ |f| f.following_user.try(:username).to_s }
Just FYI: That try is in case of a missing following_user and the to_s is to ensure that strings are compared for sorting. Otherwise, nil when compared with a String will crash.

Eager Loading in a Rails 3 app

I am trying to reduce the number of database queries in my Rails 3 app.
User model:
has_many :agreements
Agreement model:
belongs_to :user
The agreements table has two user id fields... payer_id and payee_id. Is it possible to make something like the following work:
user_payer_agreements = current_user.agreements
user_payee_agreements = current_user.agreements
I could use user_id for one side of the transaction but I need to get both sides. Is it possible to specify payer_id or payee_id instead of user_id in the process of creating an association? If not, do i need use a join or a sql statement. Any help is appreciated.
You can do this:
has_many :payer_agreements, :class_name => 'Agreement', :foreign_key => :payer_id
has_many :payee_agreements, :class_name => 'Agreement', :foreign_key => :payee_id
With this you can do:
current_user.payer_agreements
current_user.payee_agreements
Is this what you are looking for?

Ruby on Rails: Is it possible to :include the other leg of a circular join table?

I'm working on an application that models friendships between users.
class User
has_many :friendships
has_many :friends,
:through => :friendships,
:conditions => "status = #{Friendship::FULL}"
end
class Friendship
belongs_to :user
belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
end
When two users become friends, two instances of the friendship class are created, one for each direction of the friendship. This is necessary because different permissions can be set in each direction, and also for ease of lookups so we always search by the user_id without creating a second index and needing to double up searches.
Is it possible, using either find or a named scope to pull up a friend along with the counter-friendship? I'd like to do this because I'm allowing users to edit friendship permissions which are stored in their own leg of the friendship, and thus when I'm displaying a friend's info to the user I need to check the opposite leg to see how permissions are set. (As an aside, does this seem like a logical way to do things? Either you store into the opposite side, or you have to consult the opposite side before display, both kind of unpleasant.)
I've managed to whip up a batch of SQL that does the job:
def self.secure_friends(user_id)
User.find_by_sql("SELECT u.*, cf.permissions
FROM users u
INNER JOIN friendships f ON u.id = f.friend_id
INNER JOIN friendships cf ON u.id = cf.user_id AND cf.friend_id = #{user_id}
WHERE ((f.user_id = #{user_id}) AND (f.status = #{Friendship::FULL}))")
end
The function returns all of a user's friends names and ids, along with the permissions. However, this really doesn't seem ideal, and means I have to access the permissions by calling friend.permissions, where permissions isn't actually a member of friend but is just merged in as an attribute by find_by_sql. All in all, I'd really rather be doing this with a named_scope or a find call, but unfortunately, every attempt I've made to use that approach has resulted in errors as Rails chokes on having the same Friendship join table getting joined to the query twice.
I'm not sure how much you want to do, and how much you'd allow a plugin to do -- but I've used this has_many_friends plugin with great success: has_many_friends plugin at Github
You can look at the source and be inspired... Just need a Friendship model that has things like:
belongs_to :friendshipped_by_me, :foreign_key => "user_id", :class_name => "User"
belongs_to :friendshipped_for_me, :foreign_key => "friend_id", :class_name => "User"
and then later, in your user class, have:
has_many :friends_by_me,
:through => :friendships_by_me,
:source => :friendshipped_for_me
Or, just
/app/models/user
has_many_friends
Later:
#current_user.friends.each{|friend| friend.annoy!}

Specifying the foreign key in a has_many :through relationship

I have the following three models: User, Project, and Assignment.
A User has_many Projects through an assignment. However, Assignment actually has two foreign keys that relate to a User: user_id (representing the user who was assigned the project) and completer_id (representing the user who completed the project).
Often, user_id and completer_id will be the same (if the user who was assigned the project completes it). However, if another user completes it, user_id and completer_id will be different.
In my User model, I have the following:
class User < ActiveRecord::Base
has_many :assignments
has_many :incomplete_assignments, :class_name => 'Assignment',
:conditions => 'completer_id IS NULL'
has_many :completed_assignments, :class_name => 'Assignment',
:foreign_key => 'completer_id'
# this is the important one
has_many :incomplete_projects,
:through => :assignments,
:source => :project,
:conditions => 'completer_id IS NULL'
end
I would like to make another association, called :completed_projects, which uses completer_id as the foreign key for the User in the :through model, rather than :user_id. Is it possible to do this?
And, as an aside, I am aware of the :foreign_key option. However, this option is ignored when using :through, so I'd like to know if there's a way to do this without it.
Finally, I should mention that I'm open to other designs, if it can't be done this way and someone can think of a better way.
You can always use SQL for the selection if ActiveRecord can't easily express the relationship out of the box:
has_many :completed_projects,
:class_name => "Project",
:finder_sql => 'SELECT p.* FROM projects p ' +
'INNER JOIN assignments a ON p.id=a.project_id ' +
'INNER JOIN users u on u.id=a.completer_id ' +
'WHERE u.id=#{id}'
Do you have other meta data in your assignments table?
If not I would just use habtm and add the completer_id to the projects table instead, it makes sense to live there anyway.
If you need/want to use has_many :through you could look at using named_scopes (Rails version permitting) so you could say user.projects.completed and user.projects.incomplete

Rails polymorphic many to many association

I'm trying setup a generic sort of web of related objects. Let say I have 4 models.
Book
Movie
Tag
Category
I would like to able to do:
book = Book.find(1)
book.relations << Tag.find(2)
book.relations << Category.find(3)
book.relations #=> [Tag#2, Category#3]
movie = Movie.find(4)
movie.relations << book
movie.relations << Tag.find(5)
movie.relations #=> [Book#1, Tag#5]
Basically I want to be able to take any 2 objects of any model class (or model class that I allow) and declare that they are related.
Obviously I don't want to create a huge mess of join tables. This seems like it's not quite a has many through association, and not quite a polymorphic association.
Is this something that Rails can support via it's association declarations or should I be rolling my own logic here?
Support for polymorphism has improved dramatically since the early days. You should be able to achieve this in Rails 2.3 by using a single join table for all your models -- a Relation model.
class Relation
belongs_to :owner, :polymorphic => true
belongs_to :child_item, :polymorphic => true
end
class Book
has_many :pwned_relations, :as => :owner, :class_name => 'Relation'
has_many :pwning_relations, :as => :child_item, :class_name => 'Relation'
# and so on for each type of relation
has_many :pwned_movies, :through => :pwned_relations,
:source => :child_item, :source_type => 'Movie'
has_many :pwning_movies, :through => :pwning_relations,
:source => :owner, :source_type => 'Movie'
end
A drawback of this kind of data structure is that you are forced to create two different roles for what may be an equal pairing. If I want to see all the related movies for my Book, I have to add the sets together:
( pwned_movies + pwning_movies ).uniq
A common example of this problem is the "friend" relationship in social networking apps.
One solution used by Insoshi, among others, is to register an after_create callback on the join model ( Relation, in this case ), which creates the inverse relationship. An after_destroy callback would be similarly necessary, but in this way at the cost of some additional DB storage you can be confident that you will get all your related movies in a single DB query.
class Relation
after_create do
unless Relation.first :conditions =>
[ 'owner_id = ? and owner_type = ? and child_item_id = ? and child_item_type = ?', child_item_id, child_item_type, owner_id, owner_type ]
Relation.create :owner => child_item, :child_item => owner
end
end
end
I have come up with a bit of solution. I'm not sure it's the best however. It seems you cannot have a polymorphic has_many through.
So, I fake it a bit. But it means giving up the association proxy magic that I love so much, and that makes me sad. In a basic state, here is how it works.
book = Book.find(1)
book.add_related(Tag.find(2))
book.add_related(Category.find(3))
book.related #=> [Tag#2, Category#3]
book.related(:tags) #=> [Tag#2]
I wrapped it up in a reusable module, that can be added to any model class with a single has_relations class method.
http://gist.github.com/123966
I really hope I don;t have to completely re-implement the association proxy to work with this though.
I think the only way to do it exactly as you described is the join tables. It's not so bad though, just 6, and you can pretty much set-and-forget them.
depending on how closesly related your movies/books db tables are
what if you declared
class Items < ActiveRecord::Base
has_many :tags
has_many :categories
has_and_belongs_to_many :related_items,
:class => "Items",
:join_table => :related_items,
:foreign_key => "item_id",
:associated_foreign_key => "related_item_id"
end
class Books < Items
class Movies < Items
make sure you put type in your items table

Resources