Active record: group results with included associations - ruby-on-rails

I have these associations
class Application (id, priority, registration_id, uni_id)
belongs_to :registration
belongs_to :uni
end
class Uni (id, name)
has_many :applications
end
class Registration (id, fname, lname, total_points)
has_many :applications, :dependent => :destroy
has_many :unis, :through => :applications
end
Now I run query like
#registration = Registration.includes(:applications, :unis).where('applications.priority = ?', true).references(:applications).order(total_points: :desc)
So far it works okay. This outputs the registrations which have
1. application priority true and
2. order the registrations by total_points.
Now I want to get the registrations which have
1. application priority true
2. order the registrations by total_points
3. group the registrations by uni id.
For example, registrations for uni id 1, should be in one group and should be ordered by total_points, similarly registrations for uni id 2 should be in second group and should be ordered by total_points and so on. I tried to do something like this below
#registration = Registration.includes(:applications, :unis).where('applications.priority = ?', true).references(:applications).group('unis.id').order(total_points: :desc)
But this doesn't give me what I want. How should I change my query to get my expected result?

If what you want is, say, a Hash of uni_id keys and applications in the values, I don't think you can do this using ActiveRecord but you can do it with native Ruby using the group_by method on Array:
#registration = Registration.includes(:applications, :unis).where('applications.priority = ?', true).references(:applications).order(total_points: :desc).to_a.group_by('unis.id')
However be careful with this, since you're pulling all matching records into memory. This can quickly blow up in to a huge data structure, so whatever computations you can delegate to the db you should consider doing so.
For the record, you might also want to consider defining a scope:
class Application (id, priority, registration_id, uni_id)
belongs_to :registration
belongs_to :uni
scope :priority, ->{ where priority: true}
end
Registration.joins(:application).merge(Application.priority)
or a scoped association:
class Registration (id, fname, lname, total_points)
has_many :applications, :dependent => :destroy
has_many :unis, :through => :applications
has_many :priority_applications, ->{ where priority: true }
end
Registration.includes(:priority_applications)

Related

How to detect changes in has_many through association?

I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.

Rails & Active Record: how to join tables that are only related through another table?

I am trying to build an active record query using through table associations. Here are my models:
Event.rb:
has_many :event_keywords
User.rb:
has_many :user_keywords
Keyword.rb:
has_many :event_keywords
has_many :user_keywords
EventKeyword.rb:
belongs_to :event
belongs_to :keyword
UserKeyword.rb:
belongs_to :user
belongs_to :keyword
I am trying to build an Event scope that takes a user_id as a param and returns all the Events with shared keywords. This was my attempt but it's not recognizing the user_keywords association:
scope :with_keywords_in_common, ->(user_id) {
joins(:event_keywords).joins(:user_keywords)
.where("user_keywords.user_id = ?", user_id)
.where("event_keywords.keyword_id = user_keywords.keyword_id")
}
How do I resolve this?
Something like this might work. 2-step process. First, get all user's keywords. Then find all events with the same keyword.
scope :with_keywords_in_common, ->(user) {
joins(:event_keywords).
where("event_keywords.keyword_id" => user.user_keywords.pluck(:id))
}
The database seems to be overkill here and firstly I'd simplify by making keywords polymorphic, this would get rid of 2 of your tables here (event_keywords, and user_keywords).
Your associations would then look like this:
# Event.rb:
has_many :keywords, as: keywordable
# User.rb:
has_many :keywords, as: keywordable
# Keyword.rb:
belongs_to :keywordable, polymorphic: true
And finally, your scope:
scope :with_keywords_in_common, -> (user_id) do
joins(:keywords)
.where('keywords.keywordable_type = User AND keywords.word IN (?)', keywords.pluck(:name))
end

How to model this `has_one` `belongs_to` relationship?

User and Organization have a many-to-many association through Relationship. The Relationship model includes several boolean variables about the relationship, such as moderator (true/false) and member (true/false). Also, I added a boolean called default that sets the default organization.
I require a validation that if (and only if) a user is a member of one or more organizations (member == true), one (and exactly 1) of these organizations has to have default == true.
So basically this means that if a user is member of multiple organizations, one of these organizations needs to be the default ánd if the user is a member of multiple organizations such a default organization has to exist.
How to write this validation? My current validation generates the following error upon seeding:
PG::SyntaxError: ERROR: syntax error at or near "default"
LINE 1: ...ERE (user_id = 1) AND (member = 't' and default = ...
^
: SELECT COUNT(*) FROM "relationships" WHERE (user_id = 1) AND (member = 't' and default = 't')
My implementation in the Relationship model:
validate :default
private
def default
#relationships = Relationship.where('user_id = ?', self.user_id)
#members = #relationships.where('member = ?', true)
#defaults = #members.where('default = ?', true)
# If more than 1 organization has been set as default for user
if #defaults.count > 1
#defaults.drop(0).each do |invalid|
invalid.update_columns(default: false)
end
end
# If user is member but has no default organization yet
if !#defaults.any? && #members.any?
#members.first.update_columns(default: true)
end
end
Update On the looks of it, I understand I shouldn't model it this way, and instead should use a has_one belongs_to relationship as #DavidAldridge suggests in his answer. But I don't understand how to model this relationship (see my comment below the answer). Any advice is very much appreciated.
The reason for this being difficult is that your data model is incorrect. The identity of a user's default organisation is an attribute of the user, not of the relationship, because there can be only one default per user. If you had a primary, secondary, tertiary organisation, then that would be an attribute of the relationship.
Instead of placing a "relationship is default for user" attribute on the Relationship, place a "default_relationship_id" attribute on the User so it ...
belongs_to :default_relationship
... and ...
has_one :default_organisation, :through => :default_relationship
This guarantees that:
Only one organisation can be the default for the user
There has to be a relationship between the user and its default organisation
You can also use :dependent => :nullify on the inverse association of :default_relationship, and easily test whether an individual relationship is the default based on whether:
self == user.default_relationship.
So something like:
class User << ActiveRecord::Base
has_many :relationships, :inverse_of => :user, :dependent => :destroy
has_many :organisations, :through => :relationships, :dependent => :destroy
belongs_to :default_relationship, :class_name => "Relationship", :foreign_key => :default_relationship_id, :inverse_of => :default_for_user
has_one :default_organisation, :through => :default_relationship, :source => :organisation
class Relationship << ActiveRecord::Base
belongs_to :user , :inverse_of => :relationships
belongs_to :organisation, :inverse_of => :relationships
has_one :default_for_user, :class_name => "User", :foreign_key => :default_relationship_id, :inverse_of => :default_relationship, :dependent => :nullify
class Organisation << ActiveRecord::Base
has_many :relationships, :inverse_of => :organisation, :dependent => :destroy
has_many :users , :through => :relationships
has_many :default_for_users, :through => :relationships, :source => :default_for_user
Hence you can do such simple matters as:
#user = User.find(34)
#user.default_organisation
Default organisation is also easily eager-loaded (not that it couldn't be otherwise, but no scope is required to do so).
#Brad Werth's correct that your validate method would work better as a callback.
I'd recommend something like this in your Relationship model:
before_save :set_default
private
def set_default
self.default = true unless self.user.relationships.where(member: true, default: true).any?
end
This should enforce that a user's relationship is set to default if none of the user's other relationships already are.
Change default to is_default (as pointed out by another user in comments, default is postgres keyword). Create separate migration for this. (Or you could quote it everywhere if you prefer to leave it be as it is.)
Then, there are two points.
First, why you need to check for single is_default organization every time? You just need to migrate your current data set, and then keep it consistent.
To migrate your current data set, create migration and write something like this there:
def self.up
invalid_defaults = Relationship.
where(member: true, is_default: true).
group(:user_id).
having("COUNT(*) > 1")
invalid_defaults.each do |relationship|
this_user_relationships = relationship.user.relationships.where(member: true, is_default: true)
this_user_relationships.where.not(id: this_user_relationships.first.id).update_all(is_default: false)
end
end
Just make sure to run this migration in off-peak hours, as it could take considerable amount of time to finish. Alternatevely, you can just run that code snippet from the server console itself (just test in in development environment beforehand, of course).
Then, use callback (as rightfully suggested by another commenter) to set the default organization when the record is updated
before_save :set_default
private
def set_default
relationships = Relationship.where(user_id: self.user_id)
members = relationships.where(member: true)
defaults = members.where(is_default: true)
# No need to migrate records in-place
# Change #any? to #exists?, to check existance via SQL, without actually fetching all the records
if !defaults.exists? && members.exists?
# Choosing the earliest record
members.first.update_columns(is_default: true)
end
end
To take the case into account where Organization is being edited, callback to organization should be added as well:
class Organization
before_save :unset_default
after_commit :set_default
private
# Just quque is_default for update...
def remember_and_unset_default
if self.is_default_changed? && self.is_default
#default_was_set = true
self.is_default = false
end
end
# And now update it in a multi-thread safe way: let the database handle multiple queries being sent at once,
# and let only one of them to actually complete, keeping base in always consistent state
def set_default
if #default_was_set
self.class.
# update this record...
where(id: self.id).
# but only if there is still ZERO default organizations for this user
# (thread-safety will be handled by database)
where(
"id IN (SELECT id FROM organizations WHERE member = ?, is_default = ?, user_id = ? GROUP BY user_id HAVING COUNT(*)=0)",
true, true, self.user_id
)
end
end

Accessing values in a has_many :through join table

I've got users who are members of groups through a membership join table, and one of the attributes of that join table is "administrator". I'm trying to do a check inside of a group's member view, looping through each member to see if they are an administrator.
In the view I tried the following:
for user in #group.users
if user.administrator?
...DO STUFF
end
end
I also tried this in the controller:
#administrators = #group.memberships.find(:all, :conditions => ["administrator = 1"])
But no luck. Any thoughts?
UPDATE - per below, put a method into the user model:
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ? AND administrator = ?', self[:id], group_id, true])
end
I think this would be a cleaner way to do this
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
has_many :admins, :through => :memberships, :source => :user,
:conditions => ['memberships.administrator = ?', true]
end
You now have a group.admins list
for user in #group.admins
...DO STUFF
end
Although I think you could setup associations to accomplish this I think the easiest way to do it would be to add a method to your User model that allows you to check for each user (this way it would fit in the loop you have provided). I don't know if it will drop right it, may take a few quick changes but you could start with something like:
User Model
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ?', self[:id], group_id]).administrator == 1
end

Rails, ActiveRecord: how do I get the results of an association plus some condition?

I have two models, user and group. I also have a joining table groups_users.
I have an association in the group model:
has_many :groups_users
has_many :users, :through=> :groups_users
I would like to add pending_users which would be the same as the users association but contain some conditions. I wish to set it up as an association so that all the conditions are handled in the sql call. I know there's a way to have multiple accessors for the same model, even if the name is not related to what the table names actually are. Is it class_name?
Any help would be appreciated, thanks
Use named_scopes, they're your friend
Have you tried using a named_scope on the Group model?
Because everything is actually a proxy until you actually need the data,
you'll end up with a single query anyway if you do this:
class User < ActiveRecord::Base
named_scope :pending, :conditions => { :status => 'pending' }
and then:
a_group.users.pending
Confirmation
I ran the following code with an existing app of mine:
Feature.find(6).comments.published
It results in this query (ignoring the first query to get feature 6):
SELECT *
FROM `comments`
WHERE (`comments`.feature_id = 6)
AND ((`comments`.`status` = 'published') AND (`comments`.feature_id = 6))
ORDER BY created_at
And here's the relevant model code:
class Feature < ActiveRecord::Base
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :feature
named_scope :published, :conditions => { :status => 'published' }
This should be pretty close - more on has_many.
has_many :pending_users,
:through => :groups_users,
:source => :users,
:conditions => {:pending => true}
:pending is probably called something else - however you determine your pending users. As a side note - usually when you see a user/group model the association is called membership.
In the User model:
named_scope :pending, :include => :groups_users, :conditions => ["group_users.pending = ?", true]
That's if you have a bool column named "pending" in the join table group_users.
Edit:
Btw, with this you can do stuff like:
Group.find(id).users.pending(:conditions => ["insert_sql_where_clause", arguments])

Resources