Rails / ActiveRecord has_many via: join 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 manager . The connection model is called ProjectParticipation .

I now have a problem using associations on unsaved objects. The following commands work, as 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 ProjectParticipation itself, and I can access the projects user using u.projects .

But this will not work if I create ProjectParticipation 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 projects empty? I cannot access u.projects projects as before.

But if I myself go to participate, the project will appear:

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

Should it not work as the first example directly: u.projects returns me all the projects, regardless of whether I create the connection object itself or not? Or how can I make AR aware of this?

+7
ruby-on-rails activerecord has-many-through
source share
4 answers

Short answer : No, the second example will not work the way it worked in the first example. You should use the first sample method for creating intermediate associations directly with user and project objects.

Long answer :

Before we get started, we need to know how has_many :through handled in ActiveRecord::Base . So, start with the has_many(name, scope = nil, options = {}, &extension) method that calls the association here, returned reflection at the end of the method, and then add the reflection to the hash in the cache with a key-value pair here .

Now the question is how these associations are activated?!?!

This is because of the association(name) method. Which calls the association_class method, which actually calls and returns this constant: Associations::HasManyThroughAssociation , which makes this line autostart active_record / association / has_many_through_association.rb and create an instance of its instance . Here, the owner and reflection are saved when the association is created and in the next reset method, which is called in the subclass of ActiveRecord::Associations::CollectionAssociation .

Why was this reset call important? Because it sets @target as an array. This @target is an array in which all related objects are saved when the query is executed, and then used as a cache when it is reused in the code instead of creating a new query. Therefore, when calling user.projects (where the user and projects are saved in db, i.e. the call: user = User.find(1) , and then user.projects ) will make a db request and will call it again, will not.

So, when you create a reader to call an association, for example: user.projects , it calls the @target collection before populating @target from load_target .

It barely scratches the surface. But you get an idea of ​​how associations are built using builders (which creates a different reflection based on the condition) and creates a proxy for reading data in the target variable.

TL; DR

The difference between your first and second examples is how their association creators call to create an association reflex (based on a macro) , proxy variables, and the target instance.

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 is no proxy for BelongsToAssociation , it has only a goal and an owner .

However, if you really tend to do your second work example, you just need 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, this is a very bad way to create / save associations. So stick with the very first example.

+9
source share

This relates more to the structure of the rail structure at the level of ruby ​​data structures. To simplify this, we can say so. First of all, imagine that the User as a data structure contains:

  • project_participations Array
  • Array projects

And the project

  • Array Users
  • project_participations Array

Now that you mark the relation : through another (user.projects through user.project_participations)

Rails implies that when you add an entry to this first relation (user.projects), it will need to create another one in the second realism (user.project_participations), which is the result of a through hook

So in this case

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

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

So let's see what happens with the second example.

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

So, in conclusion, it acts like a one-way binding to the ruby ​​data structure level, and whenever you save and update your objects, it will behave the way you wanted.

+2
source share

At the risk of serious simplification, let me try to explain what happens

What most of the other answers try to tell you is that these objects were not yet connected by an active record until they were saved in the database. Therefore, the association behavior that you expect is not fully connected.

Note that this line is from your first example

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

Is identical to the result from your second example.

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

This statement from your analysis of what you think rails does inaccurately:

So far so good - AR created ProjectParticipation on its own, and I can access user projects using u.projects.

The AR record did not create ProjectParticipation. You have indicated this relationship in your model. AR simply returns a proxy server for the collection, which it will have at some point in the future, which is assigned when filling, etc., you can iterate and query its members, etc.

The reason this works:

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

But it is not

 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 []> 

This is the first case, you simply add objects to the array, which your user instance has direct access to. In the second example, the has_many_through relationship reflects the relationships that occur at the database level. In the second example, in order for your projects to be accessible through your user, AR must actually run a query that joins the tables and returns the data you are looking for. Since none of these objects are saved, but the database query cannot happen, so all you return is a proxy.

The last bit of code is misleading because it does not actually do what you think.

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

In this case, you have a user who directly holds an array of ProjectParticipations, one of which directly holds the project so that it works. In fact, it does not use the has_many_through mechanism, as you think.

Again, this makes things a little easier, but it's a general idea.

+1
source share

Associations are defined at the database level and use the primary key of the database table (and in the case of polymorphic , the name of the class). In the case of has_many :through search by association (e.g. User Project s):

  • Get all pairs of User - Project , whose user_id is a specific value (primary key of an existing User in the database)
  • Extract all project_id (primary project keys) from these pairs.
  • Get all Project using the received keys

Of course, these are simple terms, in terms of a database it is much shorter and uses more complex abstractions, such as inner join , but the essence is the same.

When you create a new object via new , it is not yet stored in the database and therefore has no primary key (it nil ). However, if the object is not already in the database, you have no way to refer to it from any ActiveRecord association.

Side note:
However, there is a chance that the newly created (and not yet saved) object will act as if something is associated with it: it can display entries related to NULL . This usually means that you have an error in your database schema that allows such things to happen, but hypothetically you could create your own database to use this.

0
source share

All Articles