I tried to set up a basic association between users and projects:
user.rb
class User < ActiveRecord::Base
has_many :projects, foreign_key: 'owner_id'
has_many :project_members, through: :project_members
end
project.rb
class Project < ActiveRecord::Base
has_many :project_members, dependent: :destroy
has_many :users, through: :project_members
end
project_member.rb
class ProjectMember < ActiveRecord::Base
belongs_to :user
belongs_to :project
end
my project_members table:
+----+------------+---------+
| id | project_id | user_id |
+----+------------+---------+
| 1 | 1 | 1 |
| 2 | 2 | 1 |
+----+------------+---------+
and my project table:
+----+-------+----------+
| id | name | owner_id |
+----+-------+----------+
| 1 | test1 | 1 |
| 2 | test2 | 2 |
+----+-------+----------+
why I get with
current_user.projects
only the projects where the projects.owner_id = current_user.id and not the projects where the user is member?
But I think it's the wrong side I tried to get the data I want.
I'm on /projects where the projects controller gets the data.
I think I should use something like that:
class ProjectsController < ApplicationController
# GET /projects
def index
#projects = Project.all
end
end
but how I can get only the projects where current_user.id is member of?
I think you should set that up a tiny bit different:
class User < ActiveRecord::Base
has_many :owned_projects, class_name: "Project", foreign_key: 'owner_id', dependent: :restrict_with_error
has_many :project_members, dependent: :destroy
has_many :projects, through: :project_members
end
class Project < ActiveRecord::Base
has_many :project_members, dependent: :destroy
has_many :users, through: :project_members
belongs_to :owner, class_name: "User"
end
In order to get all the projects the current_user is a member of you can do:
current_user.projects
In order to get all the projects the current_user owns you can do:
current_user.owned_projects
As an addition to Kasper Johansen's answer, you may also wish to look into has_and_belongs_to_many:
The main reason you'd use this is because you're not using any extra attributes in your join model (which is your current setup).
If you want to keep it that way, you can do away with your join model by using the has_and_belongs_to_many association in place of has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :projects
end
#join table - projects_users
#app/models/project.rb
class Project < ActiveRecord::Base
has_and_belongs_to_many :users
end
From the docs:
The simplest rule of thumb is that you should set up a has_many
:through relationship if you need to work with the relationship model
as an independent entity. If you don't need to do anything with the
relationship model, it may be simpler to set up a
has_and_belongs_to_many relationship (though you'll need to remember
to create the joining table in the database).
In regards your original problem, you have to also remember that in order to get the data back from an associative model, you have to set up the associations that will be populated with that data.
You mention...
why I get with current_user.projects only the projects where the projects.owner_id = current_user.id
... because you've set the association of has_many :projects, foreign_key: 'owner_id'
Rails is not magic - it has to take what associations you give it and populate the data in those methods accordingly:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :owned_projects, class_name: "Project", foreign_key: :owner_id
has_and_belongs_to_many :projects
end
#app/models/project.rb
class Project < ActiveRecord::Base
has_and_belongs_to_many :users
end
Hopefully gives you some more context to add to Kasper's answer.
Related
I have 3 model User Project Bug. I want to create many to many relation with through. I create the relation in model i don't know it is correct or not, user have user type column which is enum type user type contain developer, manager , QA
user.user_type.manager belong to many project it has one to many relation
user.user_type.developer has many project and many project belong to developer. it has many to many realtion
project has many bugs and bugs belong to project
developer has many bugs and many bugs belong to developer
bug model
class Bug < ApplicationRecord
belongs_to :project
has_many :developers, -> { where user_type: :Developer }, class_name: 'User', through: :project, source: :bugs
end
project model
class Project < ApplicationRecord
has_many :bugs, dependent: :delete_all
has_many :developers, -> { where user_type: :Developer }, class_name: 'User', through: :users, source: :project
has_many :users //it belong to manager_id
end
user model
class User < ApplicationRecord
enum user_type: %i[Manager Developer QA]
has_many :projects
has_many :bugs
end
developer_bug model
class DevelopersBug < ApplicationRecord
has_many :bugs
has_many :users
end
project_developer model
class ProjectsDeveloper < ApplicationRecord
has_many :projects
has_many :users
end
This
has_many :developers, -> { where user_type: :Developer },
class_name: 'User',
through: :users,
source: :project
is not what you think it is. It means something on the line of:
I already have an association 'users'. The users have an association 'project'.
Please configure an association that makes both JOINs and gives me the list of projects associated to the associated users.
This association will be named "developers" and be of objects of class "User".
You can see how these instructions are inconsistent. This
has_many :projects, through: :users, source: :project
will define a list of associated projects, by jumping over users.
On the other side, this:
has_many :developers, -> { where user_type: :Developer }, class_name: 'User'
will define a direct has-many association with a subset of all the users.
Given your description, your data model seems wrong, maybe this will be better:
class User < ApplicationRecord
has_many :managed_projects, inverse_of: :manager, class_name: 'Project'
has_and_belongs_to_many :projects
has_many :bugs
end
class Project < ApplicationRecord
belongs_to :manager, class_name: 'User', inverse_of: :managed_projects
has_and_belongs_to_many :users
has_many :bugs
end
class Bug < ApplicationRecord
belongs_to :user
belongs_to :project
end
Your schema should include the three tables, and an additional many-to-many join table projects_users that holds foreign keys to both users and projects.
As rewritten has already pointed in his excellent answer out your data model is flawed. What you want instead is a join table which joins the users and projects:
class User < ApplicationRecord
has_many :project_roles
has_many :projects, through: :project_roles
end
class ProjectRole < ApplicationRecord
belongs_to :user
belongs_to :project
end
class Project < ApplicationRecord
has_many :users
has_many :projects, through: :project_roles
end
If you then want to give the user specific roles in a project you would add the enum to the join table and this is where it starts to get hairy so bear with me here:
class ProjectRole < ApplicationRecord
enum roles: [:manager, :developer, :qa]
belongs_to :user
belongs_to :project
end
class User < ApplicationRecord
has_many :project_roles
has_many :projects, through: :project_roles
has_many :project_roles_as_manager,
-> { manager }, # short for `where(role: :manager)`
class_name: 'ProjectRole'
has_many :projects_as_manager,
class_name: 'Project',
through: :project_roles_as_manager,
source: :project
has_many :project_roles_as_developer,
-> { developer },
class_name: 'ProjectRole'
has_many :projects_as_developer,
class_name: 'Project',
through: :project_roles_as_developer,
source: :project
# ...
end
This defines associations with a default scope and then joins through that association. You would then do the same thing on the other end of the assocation:
class Project < ApplicationRecord
has_many :users
has_many :projects, through: :project_roles
has_many :manager_project_roles,
-> { manager },
class_name: 'ProjectRole'
has_many :managers,
through: :manager_project_roles,
source: :user
# ...
end
Of course this is a lot of duplication which you can cut by looping over ProjectRoles.roles.keys and defining the assocations dynamically.
This is a very flexible way of modeling it which makes as few assumptions about the domain as possible. For example it allows multiple managers for a project and it allows users to have different roles in different projects.
If you want to model "bugs" as you would typically would with issues in a tracker you would create one table for the bug and a join table for the assignment:
class Bug < ApplicationRecord
belongs_to :project
has_many :bug_assignments
has_many :users, through: :bug_assignments
end
class BugAssignment < ApplicationRecord
has_one :project, through: :bug
belongs_to :bug
belongs_to :user
end
class User < ApplicationRecord
# ...
has_many :bug_assignments
has_many :bugs
end
I've been going back and forward on this and I would like some advices.
I have "User" that can be part of many "Organizations", and for each one they can have many "Roles". (actually I have this scenario repeated with other kind of users and with something like roles, but for the sake of the example I summed it up).
My initial approach was doing a Table with user_id, organization_id and role_id, but that would mean many registers with the same user_id and organization_id just to change the role_id.
So I thought of doing an organization_users relation table and an organization_users_roles relation. The thing is, now I don't exactly know how to code the models.
class Organization < ActiveRecord::Base
has_and_belongs_to_many :users, join_table: :organization_users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :organizations, join_table: :organization_users
end
class OrganizationUser < ActiveRecord::Base
has_and_belongs_to_many :users
has_and_belongs_to_many :organizations
has_many :organization_user_roles
has_many :roles, through: :organization_user_roles
end
class OrganizationUserRole < ActiveRecord::Base
has_and_belongs_to_many :roles
has_and_belongs_to_many :organization_users
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :organization_user_roles
end
If for example I want to get: ´OrganizationUser.first.roles´ I get an error saying: PG::UndefinedTable: ERROR: relation "organization_user_roles" does not exist
How should I fix my models?
You should use a much simpler approach. According to your description, Roles is actually what connects Users to Organizations and vice-versa.
Using the has_many and has_many :through associations, this can be implemented like the following:
class User < ActiveRecord::Base
has_many :roles, inverse_of: :users, dependent: :destroy
has_many :organizations, inverse_of: :users, through: :roles
end
class Organization < ActiveRecord::Base
has_many :roles, inverse_of: :organizations, dependent: :destroy
has_many :users, inverse_of: :organizations, through: :roles
end
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
belongs_to :organization, inverse_of: :roles
end
If you wish to preserve roles when you destroy users or organizations, change the dependent: keys to :nullify. This might be a good idea if you add other descriptive data in your Role and want the role to remain even though temporarily vacated by a user, for example.
The has_many :through association reference:
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
To add to jaxx's answer (I upvoted), I originally thought you'd be best looking at has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :positions
has_many :organizations, through: :positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
#columns id | user_id | organization_id | role_id | etc | created_at | updated_at
belongs_to :user
belongs_to :organization
belongs_to :role
delegate :name, to: :role #-> #position.name
end
#app/models/organization.rb
class Organization < ActiveRecord::Base
has_many :positions
has_many :users, through: :positions
end
#app/models/role.rb
class Role < ActiveRecord::Base
has_many :positions
end
This will allow you to call the following:
#organization = Organization.find x
#organization.positions
#organization.users
#user = User.find x
#user.organizations
#user.positions
This is much simpler than your approach, and therefore has much more ability to keep your system flexible & extensible.
If you want to scope your #organizations, you should be able to do so, and still call the users / positions you need.
One of the added benefits of the code above is that the Position model will give you an actual set of data which can be shared between organizations and users.
It resolves one of the main issues with jaxx's answer, which is that you have to set a role for every association you make. With my interpretation, your roles can be set on their own, and each position assigned the privileges each role provides.
If the user can have many Roles for a single organisation,
and OrganizationUser represents this membership,
than, yes, you need another table for organization_user_roles.
You need to explicitly create it in the database (normally with a migration)
To not get confused, try to find a nice name for OrganisationUser, like employment, membership, etc.
I have two models in my rails 4.2.4 project that I'd like to join together and I'm struggling with creating the correct relationship between the two.
The domain in question is rugby and the code is all available at https://github.com/rgbycch/rgbycch-rest/tree/rails. The models are:
Player (https://github.com/rgbycch/rgbycch-rest/blob/rails/app/models/player.rb)
PlayerPosition (https://github.com/rgbycch/rgbycch-rest/blob/rails/app/models/player_position.rb)
I'd like to be able to create a relationship whereby a Player can have multiple favoured PlayingPositions. I believe my db structure would need to look something like this:
create_table :favoured_positions do |t|
t.references :player, index: true, foreign_key: true # fk to player table
t.references :player_position, index: true, foreign_key: true # fk to player_position table
t.integer :num_favs # additional data for a favored playing position
t.timestamps null: false
end
I can generate a new FavoredPosition model like this I guess:
bundle exec bin/rails generate model FavouredPosition player:references player_position:references num_favs:integer
which generates a class that looks like:
class FavouredPosition < ActiveRecord::Base
belongs_to :player
belongs_to :player_position
end
I'm not sure how to alter my Player and PlayerPosition models to reflect this new relationship. Is the following correct:
class Player < ActiveRecord::Base
has_many :favored_positions, :through => :favoured_positions
end
class PlayerPositions < ActiveRecord::Base
has_many :favored_playing_positions, :through => :favoured_positions
end
Would you advise me to add indexes to either of these tables too?
Thanks,
Sean
You would need to set up the associations like this:
class Player < ActiveRecord::Base
has_many :favoured_positions
has_many :player_positions, through: :favoured_positions
end
class PlayerPosition < ActiveRecord::Base
has_many :favoured_positions
has_many :players, through: :favoured_positions
end
See: http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Firstly, change PlayerPosition to Position, it's far simpler and works with the DRY (Don't Repeat Yourself) principle.
Secondly, what you're referring to is an ActiveRecord association. These are basically how the ActiveRecord ORM allows you to associate objects in Rails.
--
has_many :through
The association I feel you'd best using is either going to be has_many :through or has_and_belongs_to_many. Since you've comitted to has_many :through, here's what you need to do:
#app/models/player.rb
class Player < ActiveRecord::Base
#columns id | etc | etc | created_at | updated_at
has_many :favoured_positions
has_many :positions, through: :favoured_positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
#columns id | etc | etc | created_at | updated_at
has_many :favoured_positions
has_many :players, through: :favoured_positions
end
#app/models/favoured_position.rb
class FavouredPosition < ActiveRecord::Base
#columns id | position_id | player_id | any | other | column | created_at | updated_at
belongs_to :position
belongs_to :player
end
--
has_and_belongs_to_many
An important caveat about the difference between has_many :through and has_and_belongs_to_many. If you didn't have any extra columns in your favoured_positions join model, I would recommend you use has_and_belongs_to_many. It's far simpler for what you need:
#app/models/player.rb
class Player < ActiveRecord::Base
has_and_belongs_to_many :positions
end
#app/models/position.rb
class Position < ActiveRecord::Base
has_and_belongs_to_many :players
end
You'd then make a join table as follows:
#players_positions
player_id | position_id
Right. This simply refuses to work. Been at this for hours.
album model
class Album < ActiveRecord::Base
has_many :features, through: :join_table1
end
features model
class Feature < ActiveRecord::Base
has_many :albums, through: :join_table1
end
join_table1 model
class JoinTable1 < ActiveRecord::Base
belongs_to :features
belongs_to :albums
end
join_table1 schema
album_id | feature_id
albums schema
id | title | release_date | genre | artist_id | created_at | updated_at | price | image_path
features schema
id | feature | created_at | updated_at
Upon raking the test database, and running this integration test:
require 'test_helper'
class DataFlowTest < ActionDispatch::IntegrationTest
test "create new user" do
album = albums(:one)
feature = features(:one)
album.features
end
end
I get
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :join_table1 in model Album
Why is this?
You need to add has_many :album_features both to Album and Feature models (given that you rename JoinTable1 model to more meaningful AlbumFeature, and corresponding table would be album_features), as :through references association - your error is exactly about it.
Or you can use has_and_belongs_to_many so there will be no need to define special link model. But in that case you must name your table albums_features.
Just define the models as follow
album model
class Album < ActiveRecord::Base
has_many :join_table1
has_many :features, through: :join_table1
end
features model
class Feature < ActiveRecord::Base
has_many :join_table1
has_many :albums, through: :join_table1
end
join_table1 model
class JoinTable1 < ActiveRecord::Base
belongs_to :features
belongs_to :albums
end
happened to me as well.
made it work by adding the join table as has_many to both models.
like this:
connection model:
module Alerts
class AlertIncidentConnection < ActiveRecord::Base
belongs_to :incident
belongs_to :alert
end
end
alert model:
module Alerts
class Alert < ActiveRecord::Base
has_many :alert_incident_connections, class_name: 'Alerts::AlertIncidentConnection'
has_many :incidents, through: :alert_incident_connections,class_name: 'Alerts::Incident', dependent: :destroy
end
end
incident model:
module Alerts
class Incident < ActiveRecord::Base
has_many :alert_incident_connections, class_name: 'Alerts::AlertIncidentConnection'
has_many :alerts, through: :alert_incident_connections,class_name: 'Alerts::Alert' ,dependent: :destroy
end
end
migration file:
class CreateTableAlertIncidentConnections < ActiveRecord::Migration
def change
create_table :alert_incident_connections do |t|
t.references :alert, null: false, index: true
t.references :incident, null: false, index: true
t.timestamps
end
end
end
usage:
alert.incidents << incident
alert.save!
Similarly like #mad_raz answered, but join table needs to have singulars for belongs_to, like this:
class JoinTable1 < ActiveRecord::Base
belongs_to :feature
belongs_to :album
end
Complete associations tutorial https://kolosek.com/rails-join-table/
What would be the best way to connect following three models?
class Tournament < ActiveRecord::Base
has_many :submissions
has_many :creatures, :through => :submissions, :uniq => true
has_many :teams, :through => :submissions, :uniq => true
end
class Creature < ActiveRecord::Base
belongs_to :team
has_many :tournaments, :through => :team
end
class Submission < ActiveRecord::Base
belongs_to :tournament
belongs_to :team
end
class Team < ActiveRecord::Base
has_many :creatures
has_many :submissions
has_many :tournaments, :through => :submissions
end
I want to achieve something like this:
> team_1.tournaments[0] = tournament_1
> tournament_1.teams[0]
(returns team_1)
> team_1.tournaments[0].creatures
(returns [])
> team.tournaments[0].creatures[0] = creature_1
> creature_1.tournaments
(returns tournament_1)
What is the most efficient way to have a specific creature and a team associated with a specific tournament?
EDIT: The above is the desired behavior.. Current problem is that as soon as I add team to tournament.teams all the creatures in that team automatically have that tournament listed in creature.tournament, while I am trying to make it so that creatures are added to tournament selectively.. Is it at all possible with one join table?
Thanks!
Submission should be your join table between Tournamentsand Teams, correct?
Creature [id, team_id]
|
|
|
Team [id]
|
|
|
Submission [id, team_id, tournament_id]
|
|
|
Tournament [id]
Model relationships:
class Creature < ActiveRecord::Base
belongs_to :team
has_many :tournaments, :through => :team # this should work since Rails 3.1 or 3.2
end
class Team < ActiveRecord::Base
has_many :creatures
has_many :tournaments, :through => :submissions
has_many :submissions
end
class Submission < ActiveRecord::Base
belongs_to :team
belongs_to :tournament
end
class Tournament < ActiveRecord::Base
has_many :teams, :through => :submissions
has_many :creatures, :through => :teams # this should work since Rails 3.1 or 3.2
has_many :submissions
end
Now you should be able to call:
team_1.tournaments[0].creatures