Dudedolf
Dudedolf

Reputation: 625

Rails has_many and has_one with argument

I have a setup like the below:

user.rb

class User < ActiveRecord::Base
  ...

  has_many :projects_users
  has_many :projects, through: :projects_users  

  ...

end

projects_users.rb

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

  has_one :projects_users_role  
end

project.rb

class Project < ActiveRecord::Base
  has_many :projects_users
  has_many :users, through: :projects_users
end

Users are nested under Project in my routes file.

For some reason, I cannot seem to find a good way of accessing a single project for a user. Ideally I would like to create a

 has_one :project_user

association and then a

has_one :project, through: :project_user 

and somehow pass and id for the project to the association (which I know is not possible). Any ideas for a good approach.

I would then like to call a method similar to

   @user = User.includes(:project (project_id parameter).find(:id) 

in my users controller.

Any assistance is greatly appreciated.

Thank you

UPDATE

I essentially have a role that is attached per project per user in the projects_users.rb (updated above and added below)

projects_users.rb

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

  has_one :projects_users_role  
end

projects_users_role.rb

  class ProjectsUsersRole < ActiveRecord::Base
    belongs_to :projects_user
    enum role: [...]
  end

And my aim, which I should have stated previously is to be able to edit this role for the user on the given project via a route like

  /projects/1/users/2/edit-roles

this would display the user, and allow me to assign the role to the projects_users_roles table, with the nested ids added i.e. the id from the project_users table. Hence I would like (I think) to have a has_one to projects with an argument so that my nested form would be simpler.

Update 2

I have stumbled across this

 has_one :project_department, ->(department) { where department: department }, class_name: 'ProjectDepartment'  

from here http://www.rojotek.com/blog/2014/05/16/cool-stuff-you-can-do-with-rails-has_one/ but cannot make it apply as replacing 'department' with 'project' does not use the project id in the join but the user id in my case.

user.rb update

...
has_one :project_user, -> (project) { where project: project},    class_name: 'ProjectsUser'
has_one :project, through: :project_user  
...

From the console

 User.first.project_user(1)
  User Load (0.2ms)  SELECT  `users`.* FROM `users`  ORDER BY `users`.`id` ASC LIMIT 1
  ProjectsUser Load (0.1ms)  SELECT  `projects_users`.* FROM `projects_users` WHERE `projects_users`.`user_id` = 26 AND `projects_users`.`project_id` = 26 LIMIT 1

The project_id should be 1 in this case but it is using the user.id 26. Not sure why really.

Thanks again

Upvotes: 1

Views: 5884

Answers (2)

Малъ Скрылевъ
Малъ Скрылевъ

Reputation: 16507

That is correct, if you have a model User, an argument which is passed to closure is the self for called class instance, i.e. it will be an instance of User. So in following code piece project variable will have a user value.

user.rb

has_one :project_user, -> (project) { where project: project}, class_name: 'ProjectsUser'

To pass into has_many/one relation an additional argument use extending selector:

class User < ActiveRecord::Base
   has_one :project_user, class_name: 'ProjectsUser' do
      def for_project(project)
         where(project: project)
      end
   end
end

customers.project_user.for_project(project)

The approach is described in Association Extension head in Rails Association page.

Upvotes: 7

Ruby Racer
Ruby Racer

Reputation: 5740

One of various ways to produce your result in one SQL statement, would be with a left join:

@user=User.joins(:projects).
     where("users.id = ? and projects.id = ?", 
            params[:id], 
            params[:project_id]).take # use take to get one row only

UPDATE
The above statement will produce an extra SQL request if you want to get:

@user.projects.first.id

If you want no more statements for the project (of course you don't) you should combine the join with an include:

@user=User.joins(:projects).includes(:projects).
     where("users.id = ? and projects.id = ?", 
            params[:id], 
            params[:project_id]).take # use take to get one row only

So now

@user.projects.first.id

will produce no extra SQL statements

Upvotes: 0

Related Questions