Let\'s work with these classes:
class User < ActiveRecord::Base
has_many :project_participations
has_many :projects, through: :project_participations,
At the risk of some serious oversimplification let me try to explain what is going on
What Most of the other answers are trying to tell you is that these objects have not been linked yet by active record until they are persisted in the DB. Consequently the association behavior that you are expecting is not fully wired up.
Notice that this line from your first example
u.project_participations
=> #]>
Is identical to the result from your second example
u.project_participations
=> #]>
This statement from your analysis of what you think rails is doing is inaccurate:
So far so good - AR created the ProjectParticipation by itself and I can access the projects of a user with u.projects.
AR record has not created the ProjectParticipation. You have declared this relationship in your model. AR is just returning proxy for the collection that it will have at some point in the future, which when populated assigned, etc, you will be be able to iterate over and query its members etc.
The reason that this works:
u.projects << p
u.projects
=> #]>
But this doesn't
pp.project = p # assign project to project_participation
u.project_participations << pp # assign project_participation to user
u.project_participations
=> #]>
u.projects
=> #
Is that in the first case you are just adding objects to an array that your user instance has direct access to. In the second example the has_many_through relationship reflects a relationship that happens at the database level. In the second example in order for the your projects to be accessible through your user, AR has to actually run a query that joins the tables and returns the data you are looking for. Since none of these objects is persisted yet that database query can't happen yet so all you get back are the proxies.
The last bit of code is misleading because it is not actually doing what you think.
u.project_participations.map(&:project)
=> [#]
In this case you have a user which is directly holding an array of ProjectParticipations one of which is directly holding a project so it works. It is not actually using the has_many_through mechanism in they way you think.
Again this is a bit of an oversimplification but that is the general idea.