Here is an extract of the models I have:
class User < ActiveRecord::Base
has_many :participations
has_many :groups, through: :participations
has_many :subgroups, through: :participations
end
class Group < ActiveRecord::Base
has_many :participations
has_many :users, through: :participations
has_many :subgroups
end
class Subgroup < ActiveRecord::Base
has_many :participations
has_many :users, through: :participations
end
class Participation < ActiveRecord::Base
belongs_to: :user
belongs_to: :group
belongs_to: :subgroup
validates :user, presence: true
validates :group, presence: true
# Subgroup can be empty, as long as user as not been placed.
# There should be only one participation per couple User:Group
validates_uniqueness_of :group_id, :scope => [:user_id]
# Also has a state-machine, describing the participation status.
end
Explanation: groups are split in subgroups, users select the group they join, but not the subgroup, which is selected later by an administrator.
When a User is added to a Group (group_a.users << user_a), a Participation is automatically created by ActiveRecord.
I would like the same participation to be reused when the same User is added to a Subgroup of that Group (subgroup_1.users << user_a with subgroup_1 a Subgroup of group_a's).
What happens actually is ActiveRecord trying to create a new Participation record, which conflicts with the previously created one (validates_uniqueness_of :group_id, :scope => [:user_id]
fires an error).
Is there anyway I could make this work? I tried hooking before_validation, before_save, and some other stuff, but every attempt failed.
Maybe there is a better way to actually model this relationship?
Any help is welcome.
Thank you,
David
You could DRY up all of your code by instead calling
class User < ActiveRecord::Base
has_many :participations
has_many :groups, through: :participations
has_many :subgroups, through: :groups # HMT -> HMT
end
Would this solve your problem? This probably won't scale, but we'll worry about that later :).
Related
I am trying to set up a polymorphic has-many-through relationship with ActiveRecord. Here's the end goal:
Users can belong to many organizations and many teams
Organizations have many users and many teams
Teams have many users and belong to an organization
I am trying to use has-many-through instead of has-and-belongs-to-many, since I need to associate some information along with the relationships (like user role in the organization or team), so I made a join table Membership.
How would I implement this?
I would design the schema like this:
Organization has many Team
Team has many TeamMember
User has many TeamMember
TeamMember belongs to User and Team
The models will be:
organization.rb
class Organization < ActiveRecord::Base
has_many :teams
has_many :team_members, through: :teams
has_many :users, through: :team_members
end
team.rb
class Team < ActiveRecord::Base
belongs_to :organization # fk: organization_id
has_many :team_members
has_many :users, through: :team_members
end
user.rb
class User < ActiveRecord::Base
has_many :team_members
has_many :teams, through: :team_members
has_many :organizations, though: :teams
end
team_member.rb
class TeamMember < ActiveRecord::Base
belongs_to :team # fk: team_id
belongs_to :user # fk: user_id
attr_accessible :role # role in team
end
So, compare with your requirements:
Users can belong to many organizations and many teams
=> Okay
Organizations have many users and many teams
=> Okay
Teams have many users and belong to an organization
=> Okay
Btw, we don't use any polymorphic here, and TeamMember stands for Membership in your early idea!
For polymorphic association,
class User
has_many :memberships
end
class Team
belongs_to :organization
has_many :memberships, :as => :membershipable #you decide the name
end
class Organization
has_many :memberships, :as => :membershipable
has_many :teams
end
class Membership
belongs_to :user
belongs_to :membershipable, polymorphic: true
end
Note that User is indirectly associated to Team and Organization, and that every call has to go through Membership.
In my projects, I use a Relationship class (in a gem I've named ActsAsRelatingTo) as the join model. It looks something like this:
# == Schema Information
#
# Table name: acts_as_relating_to_relationships
#
# id :integer not null, primary key
# owner_id :integer
# owner_type :string
# in_relation_to_id :integer
# in_relation_to_type :string
# created_at :datetime not null
# updated_at :datetime not null
#
module ActsAsRelatingTo
class Relationship < ActiveRecord::Base
validates :owner_id, presence: true
validates :owner_type, presence: true
validates :in_relation_to_id, presence: true
validates :in_relation_to_type, presence: true
belongs_to :owner, polymorphic: true
belongs_to :in_relation_to, polymorphic: true
end
end
So, in your User model, you would say something like:
class User < ActiveRecord::Base
has_many :owned_relationships,
as: :owner,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
has_many :organizations_i_relate_to,
through: :owned_relationships,
source: :in_relation_to,
source_type: "Organization"
...
end
I believe you may be able to leave the source_type argument off since the joined class (Organization) can be inferred from :organizations. Often, I'm joining models where the class name cannot be inferred from the relationship name, in which case I include the source_type argument.
With this, you can say user.organizations_i_relate_to. You can do the same set up for a relationship between any set of classes.
You could also say in your Organization class:
class Organization < ActiveRecord::Base
has_many :referencing_relationships,
as: :in_relation_to,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
has_many :users_that_relate_to_me,
through: :referencing_relationships,
source: :owner,
source_type: "User"
So that you could say organization.users_that_relate_to_me.
I got tired of having to do all the set up, so in my gem I created an acts_as_relating_to method so I can do something like:
class User < ActiveRecord::Base
acts_as_relating_to :organizations, :teams
...
end
and
class Organization < ActiveRecord::Base
acts_as_relating_to :users, :organizations
...
end
and
class Team < ActiveRecord::Base
acts_as_relating_to :organizations, :users
...
end
and all the polymorphic associations and methods get set up for me "automatically".
Sorry for the long answer. Hope you find something useful in it.
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 am struggling to find the easiest solution for associating three models:
User
Organization
Role
User and Organization is a HABTM association - one user can have multiple organizations and vice versa.
One user can also have multiple roles, but just one per organization.
Right now I have this in my model:
user.rb
class User < ActiveRecord::Base
has_many :roles, through: :organizations
has_and_belongs_to_many :organizations, :join_table => :organizations_users
end
organization.rb
class Organization < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :organizations_users
has_many :roles
end
role.rb
class Role < ActiveRecord::Base
has_many :users, through: :organizations
belongs_to :organizations
end
Does that make sense?
Here are my though:
Given that you're using the has_and_belongs_to_many and given Rails' defaults, your specification of the join_table is redundant
Your has_many :roles, through: :organizations will only work if you have both a role and a user field in the organizations tables, as Rails will expect to do a SQL select of that table looking for those fields.
Since you want users to have up to one one role per organization, then I would think the most straightforward thing would be to add a role field to the organizations_users model, as follows:
user.rb
class User < ActiveRecord::Base
has_many :roles, through: :organizations_users
has_many :organizations, :through => :organizations_users
has_many :organizations_users
end
organization.rb
class Organization < ActiveRecord::Base
has_many :users, :through => :organizations_users
has_many :roles, :through => :organizations_users
has_many :organizations_users
end
organization_user.rb
class OrganizationUser < ActiveRecord::Base
belongs_to :user
belongs_to :organization
belongs_to :role
end
role.rb
class Role < ActiveRecord::Base
end
The above assumes that you have some reason to want Role to continue to be an ActiveModel as opposed to just a string field in the organizations_users table.
I have three basic models that I am working with:
class User < ActiveRecord::Base
has_many :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
class Group < ActiveRecord::Base
has_many :assignments
end
Using this schema, I would assume the "Assignment" model is sort of the join table, which holds the information for which users belong to which groups. So, what I am trying to do is, using a User object, find out what groups they belong to.
In Rail console, I am doing the following:
me = User.find(1)
Which returns the user object, as it should. Then, I attempt to see which "groups" this user belongs to, which I thought it would go through the "Assignment" model. But, I'm obviously doing something wrong:
me.groups
Which returns:
NoMethodError: undefined method `groups' for #<User:0x007fd5d6320c68>
How would I go about finding out which "groups" the "me" object belongs to?
Thanks very much!
You have to declare the User - Groups relation in each model:
class User < ActiveRecord::Base
has_many :assignments
has_many :groups, through: :assignments
end
class Group < ActiveRecord::Base
has_many :assignments
has_many :users, through: :assignments
end
Also, I recommend you to set some validations on the Assignment model to make sure an Assignment always refers to a Group AND a User:
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :user
validates :user_id, presence: true
validates :group_id, presence: true
end
class User < ActiveRecord::Base
has_many :assignments
has_many :groups, through: :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
class Group < ActiveRecord::Base
has_many :assignments
has_many :users, through: :assignments
end
Please refer association basics
Your me is of type User not Assignment. You want to do:
me.assignments.first.groups
This will give you all the groups belonging to the user's first assignment. To get all the groups you could do as MrYoshiji has commented below:
me.assignments.map(&:groups)
You didn't define a has_many on groups. Try
me.assignments.first.group
should work.
I have two tables, users and groups. An user owns a group and can be apart of multiple groups. A group belongs to one user and can have many users.
Thus for my user model I have
has_and_belongs_to_many :groups
has_many :groups
While for my group model I have
has_and_belongs_to_many :users
belongs_to :user
I also have a join table in my migrations..
def change
create_table :groups_users, :id => false do |t|
t.integer :group_id
t.integer :user_id
end
end
My question is does this make sense? I feel like I'm doing something wrong by having has_many and belongs_to on top of has_and_belongs_to_many.
The way I would approach this, and this is my own personal methodology, is with 3 tables/models like so:
group_user.rb
class GroupUser < ActiveRecord::Base
attr_accessible :user_id, :group_id
belongs_to :group
belongs_to :user
end
group.rb
class Group < ActiveRecord::Base
attr_accessible :owner_id
validates_presence_of :owner_id
has_many :group_users
has_many :users, through: :group_users
end
user.rb
class User < ActiveRecord::Base
attr_accessible :some_attributes
has_many :group_users
has_many :groups, through: :group_users
end
Then, whenever you create a Group object, the User that created it would have its id placed in the owner_id attribute of Group and itself into the GroupUser table.
Another option, so as to not have multiple foreign keys pointing to the same relationship, is to use a join model and then add a flag on the join model to denote if the user is the owner.
For example:
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
has_many :owned_groups, through: memberships, conditions: ["memberships.owner = ?", true], class_name: "Group"
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
#This model contains a boolean field called owner
#You would create a unique constraint on owner, group and user
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
has_one :owner, through: :memberships, conditions: ["memberships.owner = ?", true], class_name: "User"
end