Inject attribute from association table into the associated record? - ruby-on-rails

I have the following model in my Rails app:
class Course < ActiveRecord::Base
has_many :memberships
has_many :members, through: :memberships, class_name: 'User'
end
While course.members successfully returns the course's members, I don't get access to the Membership model which has a role attribute.
How do I find the user role without having to find the Membership given my Course and User? Can I inject the role attribute to User somehow in the context association?
User model:
class User < ActiveRecord::Base
has_many :memberships, dependent: :destroy
has_many :courses, through: :memberships
end

Here's a solution, not the most beautiful and idiomatic though
class Course < ActiveRecord::Base
has_many :memberships
def members
User.joins(:memberships).where(id: memberships.ids).select('users.*, memberships.role')
end
end
A better approach, suggested through the comments:
has_many :members, -> { select('users.*, memberships.role') }, class_name: 'User', through: :memberships, source: :user

I had the exact same problem, and couldn't find any way to fetch the intermediate association without explicitly finding it. It's not that ugly, though:
class User < ActiveRecord::Base
has_many :memberships, dependent: :destroy
has_many :courses, through: :memberships
def role_in(course)
memberships.find_by!(course: course).role
end
end
user.role_in(course)
# => "teacher"

I think dynamically storing the contextual role depending on what Course is used is not a good idea. It feels hacky to me because the attribute value becomes set implicitly rather than by explicit execution. A User would appear different depending on where and how it's instantiated, despite representing a row in the database.
I would instead implement something like below:
class User
def course_role(course)
memberships.find_by_course(course).role
end
end
class Course
def user_role(user)
members.find_by_user(user).role
end
end
This way, it's explicit that calling methods on either the User or Course to get the role depends on the respective memberships they are associated with.

Related

Rails 4.2 has_many through

Problem
I have two models a client and a user.
A client can have many administrators and a user can be the administrator of many clients.
I found a couple of people that suggest using has_many :through is the better way to model this relationship in my situation versus has_and_belongs_to_many.
User model
class V1::User < ActiveRecord::Base
has_many :administrators,
class_name: 'V1::ClientAdministrator'
has_many :clients,
through: :administrators
class_name: 'V1::Clients'
Client model
class V1::Client < ActiveRecord::Base
has_many :users,
class_name: "V1::User"
has_many :administrators,
through: :users,
class_name: "V1::ClientAdministrator"
validates :administrators,
length: { minimum: 1}
ClientAdministrator model
class V1::ClientAdministrator < ActiveRecord::Base
belongs_to :v1_user
belongs_to :v1_client
end
Demo using rails c
u = V1::User.create!(name: 'test_user')
c = V1::Client.new(name: 'test_client')
c.administrators << u
ActiveRecord::AssociationTypeMismatch: V1::ClientAdministrator(#70347494104080) expected, got V1::User(#70347494299440)
Before switching to has_many :through I was successfully using has_and_belongs_to_many:
class V1::Client < ActiveRecord::Base
has_and_belongs_to_many :administrators,
-> { uniq },
join_table: :v1_client_administrators,
foreign_key: "v1_client_id",
association_foreign_key: "v1_user_id",
class_name: "V1::User"
The problem with this approach was I was not able to do any validation on the association such as before_destroy make sure there is still one administrator. Additionally, it's likely that I'll add metadata to that relationship in the future.
The ASK
How can I get / set the administrators of the client?
Is there any way that client.administrators would be an array of users instead of forcing me to client.administrators.each { |admin| admin.user} to get access to the user? (If I eventually add metadata this probably can't doesn't make sense)
Is there a way to restrict the use of client.users in favor of client.administrators?
Do model concerns help here?
Is this the right approach for ensuring there is always at least one administrator?
I believe here is what you're looking for, please namespace appropriately:
class User
has_many :clients_users
has_many :clients, through: :clients_users
end
class Client
has_many :clients_users
has_many :administrators, through: :clients_users, source: :user
validates :adminstrators, presence: true # I think this should ensure at least one admin
end
class ClientsUser
belongs_to :client
belongs_to :user
end
Client.first.administrators # fetch all adminstrators
Client.first.adminstrators << User.first # add an administrator

Rails has_many :through validation nested records count by specific param

I have models Workout and User which related as many to many through model
UserWorkout. And model UserWorkout has attribute :is_creator, which show what user was the creator. But Workout should have only one creator. What is the best way to add such validation?
class Workout < ActiveRecord::Base
has_many :user_workouts, inverse_of: :workout, dependent: :destroy
has_many :participants, through: :user_workouts, source: :user
def creator
participants.where(user_workouts: { is_creator: true }).order('user_workouts.created_at ASC').first
end
end
class UserWorkout < ActiveRecord::Base
belongs_to :user
belongs_to :workout
end
class User < ActiveRecord::Base
has_many :user_workouts, inverse_of: :user, dependent: :destroy
has_many :workouts, through: :user_workouts
end
Depending on your DBMS, you could add a filtered/partial index on workout_id where is_creator = true
On the active record level, you can add a custom validation
class UserWorkout
validate :workout_has_only_one_creator
private
def workout_has_only_one_creator
if self.class.find_by(workout_id: workout_id, is_creator: true)
errors.add(:is_creator, 'can only have one creator')
end
end
First of all there is a design flaw in your DB structure. I thinkis_creator should not be in UserWorkout . It is a responsibility of Workout
In other words Workout can be created by a user and a User can create many Workout so it's a one-many relation between User and Workout
Keep a created_by_id in Workout and add a association in it. It will make lot of things easier and simpler.
class Workout < ActiveRecord::Base
has_many :user_workouts, inverse_of: :workout, dependent: :destroy
has_many :participants, through: :user_workouts, source: :user
belongs_to :creator , class_name: "User", foreign_key: "created_by_id"
end
and there wont be any need to check the uniqueness as it's the single column in Workout
Now you don't need a complex query like every time you need to find the creator of a workout. It's a simple belongs_to association. Everything will be taken care by rails :)

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.

Rails isn't running destroy callbacks for has_many through join model

I have two AR models and a third has_many :through join model like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies, through: :ratings
end
class Movie < ActiveRecord::Base
has_many :ratings
has_many :users, through: :ratings
end
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :movie
after_destroy do
puts 'destroyed'
end
end
Occasionally, a user will want to drop a movie directly (without directly destroying the rating). However, when I do:
# puts user.movie_ids
# => [1,2,3]
user.movie_ids = [1, 2]
the rating's after_destroy callback isn't called, although the join record is deleted appropriately. If I modify my user model like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies,
through: :ratings,
before_remove: proc { |u, m| Rating.where(movie: m, user: u).destroy_all }
end
Everything works fine, but this is really ugly, and Rails then tries to delete the join model a second time.
How can I use a dependent: :destroy strategy for this association, rather than dependent: :delete?
Answering my own question, since this was difficult to Google, and the answer is super counter-intuitive (although I don't know what the ideal interface would be).
First, the situation is described thoroughly here: https://github.com/rails/rails/issues/7618. However, the specific answer is buried about halfway down the page, and the issue was closed (even though it is still an issue in current Rails versions).
You can specify dependent: :destroy for these types of join model destructions, by adding the option to the has_many :through command, like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies,
through: :ratings,
dependent: :destroy
end
This is counter-intuitive because in normal cases, dependent: :destroy will destroy that specific association's object(s).
For example, if we had has_many :ratings, dependent: :destroy here, all of a user's ratings would be destroyed when that user was destroyed.
We certainly don't want to destroy the specific movie objects here, because they may be in use by other users/ratings. However, Rails magically knows that we want to destroy the join record, not the association record, in this case.

Rails multiple association types

Can you have a has_many belongs_to relationship AND a has_many :through relationship without any method call conflicts?
For example, what would user.hacks return - the hacks that the user posted, or the hacks that the user marked as favorites?
I need to be able to access both results.
class User < ActiveRecord::Base
has_many :hacks, through: :favorites
has_many :hacks
end
class Hack < ActiveRecord::Base
has_many :hacks, through: :favorites
belongs_to :users
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :hack
end
The has_many defines the methods depending on the relationship name.
This means, that if you are defining a relationship several times with the same name but different options, the last definition will override the methods of the previous definition calls.
So you can't define them with the same name.
If you need to access to both, hacks and favorite hacks, you have to create the relationships as follows
class User < ActiveRecord::Base
has_many :hacks
has_many :favorites
has_many :favorited_hacks, through: :favorites, source: :hack
end
I'm not sure what it will return, try it in the console.
However i would something like that in the user class (not tested, but you can look on how to alias relations):
has_many :favorite_hacks, through: :favorites, class: :hacks
There seems to be a typo in Hack.
On another note, the way you do it seems strange to me. The user hack relation seems like ownership, and the user - fav seems like bookmarking.
If all you want is to get a list of all favourite hacks (and all hacks belong to the user), then I think it would be a much better idea to model this behaviour like so:
class User < ActiveRecord::Base
has_many :hacks
end
class Hack < ActiveRecord::Base
belongs_to :user
scope :favorite, -> { where(favorite: true) }
end
and simply have a favorite attribute on your Hack model. Then you could find the favorites by invoking user.hacks.favorite.

Resources