Rails belongs_to and has_many of the same model - ruby-on-rails

I am trying to create a database schema in rails between two models Users and Circles. A circle is a user defined group known only to the user who created this. The Circle contains other users that the user selected to be in that circle and a circle name.
So my solution is the following:
Users model: has_many: Circles
Circles model: belongs_to: User, has_many: Users
I know that there exists a has_many through method but I don't know if that's necessary for my case.

You actually need two different associations. The first is a one-to-many association. The lazy way to set it up would be:
class Circle < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :circles
end
This links users to circles through the foreign key column user_id on circles. Buts its pretty ambigous - what does user.circles mean? Is it circles a user has created or those he is a member of? Better be a bit more explicit even if it takes some configuration:
class RenameUserToCreator < ActiveRecord::Migration[6.0]
def change
rename_column :circles, :user_id, :creator_id
end
end
# rails g model circle
class Circle < ApplicationRecord
belongs_to :creator, class_name: 'User'
end
class User < ApplicationRecord
has_many :created_circles,
class_name: 'Circle',
foreign_key: :creator_id
end
Next you want to add members to a circle. This is a many-to-many association and can be done using has_many through: or has_or_belongs_to_many. Both use a join table but has_or_belongs_to_many does not have a model and is very limited in its actual usefulness. When naming join tables the lazy conventions is to just use an amalgamation of a & b - CircleUser but do use a better name if you can think of one that fits the domain.
class Circle
belongs_to :creator, class_name: 'User'
has_many :memberships
has_many :users, through: :memberships
end
# rails g model membership user:belongs_to circle:belongs_to
class Membership
belongs_to :user
belongs_to :circle
end
class User
has_many :created_circles,
class_name: 'Circle',
foreign_key: :creator_id
has_many :memberships
has_many :circles, through: :memberships
end
One thing to bear in mind is that each association MUST have a unique name. If we didn't go through that previous step and wrote:
class User
has_many :circles
has_many :memberships
has_many :circles, through: :memberships
end
The latter association would just clobber the previous association.

Related

Rails - Has many on model with multiple references

I have a model CompanyIntro which has two references to a Company:
class CompanyIntro < ApplicationRecord
belongs_to :company_one, class_name: "Company", foreign_key: "company_one_id"
belongs_to :company_two, class_name: "Company", foreign_key: "company_two_id"
...
I would like to do something like:
class Company < ApplicationRecord
has_many :company_intros, class_name: 'CompanyIntro', foreign_key: 'company_one_id'
has_many :company_intros, class_name: 'CompanyIntro', foreign_key: 'company_two_id'
...
But this is not valid
In my Company model, how to I create a has_many for both foreign keys? I am using Rails 6 which dos not allow custom sql for has_many (afaik). I also do not want to write a custom company_intros method on the Company model as I'm using another gem which looks for my has_many relationships.
You can't define has_many assocations where the foreign key is one of two columns. Its just not supported by ActiveRecord as the feature would add tons of complexity.
Using the same name for two assocations also just overwrites the previous assocation. If you want to have a single assocation here you need to add a join table.
class Company < ApplicationRecord
has_many :company_intro_participations
has_many :company_intros, through: :company_intro_participations
end
# for lack of a better name
class CompanyIntroParticipation < ApplicationRecord
belongs_to :company
belongs_to :company_intro
end
class CompanyIntro < ApplicationRecord
has_many :company_intro_participations
has_many :companies, through: :company_intro_participations
end
The alternative is creating a method which joins on company_one_id = companies.id OR company_two_id = companies.id but you will not be able to use that in the same way as an association when it comes to stuff like eager loading.

Why is an association model for a many-to-many relationship only halfway pluralized?

I have a model User and a model Group, between the two I want to create a many-to-many association with a join-table, using through.
user.rb:
class User < ActiveRecord::Base
has_many :groups_user
has_many :groups, :through => :groups_user
end
group.rb:
class Group < ActiveRecord::Base
has_many :groups_user
has_many :users, :through => :groups_user
end
groups_user.rb
class GroupsUser < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
I initially tried to do the same thing naming the joining model class GroupsUsers in groups_users.rb but I got an uninitialized constant error unless I used :class_name to specify the class name.
My question is: What is the logic behind pluralizing the first name, but not the second? The association is many-to-many so both models are on equal footing. In this case group comes first merely because of the lexical order. So why pluralize one and not the other? This makes no sense to me.
The more conventional naming approach would be:
#user.rb:
class User < ActiveRecord::Base
has_many :group_users
has_many :groups, :through => :group_users
end
#group.rb:
class Group < ActiveRecord::Base
has_many :group_users
has_many :users, :through => :group_users
end
#group_user.rb
class GroupUser < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
Conventionally, the model has the singular form. So GroupsUser, not GroupsUsers. But, an instance of the join model has only one Group and one User, So, GroupUser, not GroupsUser.
If you look at the The has_many :through Association section of the Active Record Associations guide, for instance, you will see that the join model (Appointment) is singular. This is the pattern you want to follow.
If you decide to do things unconventionally, then rails needs you to help it - for instance by requiring that you specify class_name.

How to reference a field in the join model of a has_many though Rails 5 relationship

I am working on an app where users have many quizzes and quizzes can have many users. I have set the relationships:
class User < ApplicationRecord
has_many :studies
has_many :quizzes, through: :studies
end
class Quiz < ApplicationRecord
has_many :studies
has_many :users, through: :studies
end
class Study < ApplicationRecord
belongs_to :user
belongs_to :quiz
end
I have a field in the Study table to store the score that the user made on the quiz, but I am unable to access the field. I have tried #quiz.studies.score and #quiz.study.score but Rails give me an undefined method. How to I access the field in a join model of a has_many though relationship?
#quiz.studies return the collection of studies. So you have to use first, last, each to get the score of the specific studies.
Try this:
#quiz.studies.first.score

Multiple has_many through relationships Rails

I'm still learning how to use has_many and has_many through relationships effectly. I am currently building a system where I would like users to be able to access certain maps that they are added to.
The map model is what I need the user to be able to access if they are apart of a certain group.
class Map < ApplicationRecord
has_many :rows
has_many :mapgroups
has_many :groups, through: :mapgroups
end
Since a user can belong to many groups I have a has_many through relationship
class Usergroup < ApplicationRecord
belongs_to :user
belongs_to :group
end
class User < ApplicationRecord
has_many :usergroups
has_many :groups, through: :usergroups
end
class Group < ApplicationRecord
has_many :usergroups
has_many :users, through: :usergroups
has_many :mapgroups
has_many :maps, through: :mapgroups
end
I thought about making a mapgroup model to take care of this but, so far, I am not so sure this is going to work.
class Mapgroup < ApplicationRecord
belongs_to :map
belongs_to :group
end
I am looking for a method to check to see what groups the user is apart of and then, based on those groups, give the user access to the corresponding maps. Am I on the right track with the relationships? How could I do this?
If you want to use MapGroup model only for keeping users an map connected (ModelGroup has only foreign keys on Group and Map) it's not the best approach. In this case it's better to opt for has_and_belongs_to_many assosiation. It will let you to assosiate Groups and Maps without creating useless model.
A has_and_belongs_to_many association creates a direct many-to-many connection with another model, with no intervening model.
class Group < ApplicationRecord
...
has_and_belongs_to_many :maps
end
class Map < ApplicationRecord
...
has_and_belongs_to_many :groups
end

How should I approach this relations in ruby?

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.

Resources