Let\'s work with these classes:
class User < ActiveRecord::Base
has_many :project_participations
has_many :projects, through: :project_participations,
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.
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 = #]>
#=> @target = [#]
u.association(:project_participations)
#=> ActiveRecord::Associations::HasManyAssociation object
#=> @proxy = #]>
#=> @target = [#]
u.project_participations.first.association(:project)
#=> ActiveRecord::Associations::BelongsToAssociation object
#=> @target = #
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 = #
#=> @target = [#]
u.project_participations.first.association(:project)
#=> ActiveRecord::Associations::BelongsToAssociation object
#=> @target = #
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
#=> #]>
In my opinion which is a very bad way of creating/saving associations. So, stick with the first example itself.