Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails/ActiveRecord has_many through: association on unsaved objects

Let's work with these classes:

class User < ActiveRecord::Base
    has_many :project_participations
    has_many :projects, through: :project_participations, inverse_of: :users
end

class ProjectParticipation < ActiveRecord::Base
    belongs_to :user
    belongs_to :project

    enum role: { member: 0, manager: 1 }
end

class Project < ActiveRecord::Base
    has_many :project_participations
    has_many :users, through: :project_participations, inverse_of: :projects
end

A user can participate in many projects with a role as a member or a manager. The connecting model is called ProjectParticipation.

I now have a problem using the associations on unsaved objects. The following commands work like I think they should work:

# first example

u = User.new
p = Project.new

u.projects << p

u.projects
=> #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil>]>

u.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil>]>

So far so good - AR created the ProjectParticipation by itself and I can access the projects of a user with u.projects.

But it does not work if I create the ProjectParticipation by myself:

# second example

u = User.new
pp = ProjectParticipation.new
p = Project.new

pp.project = p # assign project to project_participation

u.project_participations << pp # assign project_participation to user

u.project_participations
=> #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil>]>

u.projects
=> #<ActiveRecord::Associations::CollectionProxy []>

Why are the projects empty? I cannot access the projects by u.projects like before.

But if I go through the participations directly, the project shows up:

u.project_participations.map(&:project)
=> [#<Project id: nil>]

Shouldn't it work like the first example directly: u.projects returning me all projects not depending on whether I create the join object by myself or not? Or how can I make AR aware of this?

like image 984
Markus Avatar asked Oct 30 '14 11:10

Markus


People also ask

What is the difference between Has_one and Belongs_to?

They essentially do the same thing, the only difference is what side of the relationship you are on. If a User has a Profile , then in the User class you'd have has_one :profile and in the Profile class you'd have belongs_to :user . To determine who "has" the other object, look at where the foreign key is.

What is polymorphic association in Rails?

Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.

What is dependent destroy in Rails?

Dependent is an option of Rails collection association declaration to cascade the delete action. The :destroy is to cause the associated object to also be destroyed when its owner is destroyed.


2 Answers

Short answer: No, second example won't work the way it worked in first example. You must use first example's way of creating intermediate associations directly with user and project objects.

Long answer:

Before we start, we should know how has_many :through is being handled in ActiveRecord::Base. So, let's start with has_many(name, scope = nil, options = {}, &extension) method which calls its association builder here, at the end of method the returned reflection and then add reflection to a hash as a cache with key-value pair here.

Now question is, how do these associations gets activated?!?!

It's because of association(name) method. Which calls association_class method, which actually calls and return this constant: Associations::HasManyThroughAssociation, that makes this line to autoload active_record/associations/has_many_through_association.rb and instantiate its instance here. This is where owner and reflection are saved when the association is being created and in the next reset method is being called which gets invoked in the subclass ActiveRecord::Associations::CollectionAssociation here.

Why this reset call was important? Because, it sets @target as an array. This @target is the array where all associated objects are stored when you make a query and then used as cache when you reuse it in your code instead of making a new query. That's why calling user.projects(where user and projects persists in db, i.e. calling: user = User.find(1) and then user.projects) will make a db query and calling it again won't.

So, when you make a reader call on an association, e.g.: user.projects, it invokes the collectionProxy, before populating the @target from load_target.

This is barely scratching the surface. But, you get the idea how associations are being build using builders(which creates different reflection based on the condition) and creates proxies for reading data in the target variable.

tl;dr

The difference between your first and second examples is the way their association builders are being invoked for creating associations' reflection(based on macro), proxy and target instance variables.

First example:

u = User.new
p = Project.new
u.projects << p

u.association(:projects)
#=> ActiveRecord::Associations::HasManyThroughAssociation object
#=> @proxy = #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]>
#=> @target = [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]

u.association(:project_participations)
#=> ActiveRecord::Associations::HasManyAssociation object
#=> @proxy = #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>]>
#=> @target = [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>]

u.project_participations.first.association(:project)
#=> ActiveRecord::Associations::BelongsToAssociation object
#=> @target = #<Project id: nil, name: nil, created_at: nil, updated_at: nil>

Second example:

u = User.new
pp = ProjectParticipation.new
p = Project.new

pp.project = p # assign project to project_participation

u.project_participations << pp # assign project_participation to user

u.association(:projects)
#=> ActiveRecord::Associations::HasManyThroughAssociation object
#=> @proxy = nil
#=> @target = []

u.association(:project_participations)
#=> ActiveRecord::Associations::HasManyAssociation object
#=> @proxy = #<ActiveRecord::Associations::CollectionProxy [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>
#=> @target = [#<ProjectParticipation id: nil, user_id: nil, project_id: nil, role: nil, created_at: nil, updated_at: nil>]

u.project_participations.first.association(:project)
#=> ActiveRecord::Associations::BelongsToAssociation object
#=> @target = #<Project id: nil, name: nil, created_at: nil, updated_at: nil>

There's no proxy for BelongsToAssociation, it has just target and owner.

However, if you are really inclined to make your second example work, you will just have to do this:

u.association(:projects).instance_variable_set('@target', [p])

And now:

u.projects
#=>  #<ActiveRecord::Associations::CollectionProxy [#<Project id: nil, name: nil, created_at: nil, updated_at: nil>]>

In my opinion which is a very bad way of creating/saving associations. So, stick with the first example itself.

like image 141
Surya Avatar answered Oct 21 '22 02:10

Surya


This is more of a rails structure thing at the level of the ruby data structures. To simplify it lets put it this way. First of all imagine User as a data structure contains:

  1. project_participations Array
  2. projects Array

And Project

  1. users Array
  2. project_participations Array

Now when you mark a relation to be :through another (user.projects through user.project_participations)

Rails implies that when you add a record to that first relation (user.projects) it will have to create another one in the second realation (user.project_participations) that is all the effect of the 'through' hook

So in this case,

user.projects << project
#will proc the 'through'
#user.project_participations << new_entry

Keep in mind that the project.users is still not updated because its a completely different data structure and you have no reference to it.

So lets take a look what will happen with the second example

u.project_participations << pp
#this has nothing hooked to it so it operates like a normal array

So In conclusion, this acts like a one way binding on a ruby data structure level and whenever you save and refresh your objects, this will behave the way you wanted.

like image 21
khaled_gomaa Avatar answered Oct 21 '22 03:10

khaled_gomaa