Can I use scopes on an included model? - ruby-on-rails

I have scopes defined on my Job model, and I want to use them when including jobs in an Active Record query, rather than writing out long-hand the conditions and ordering.
Here is the code I have that works but is very verbose:
#employees_and_jobs = supervisor.direct_reports.alphabetical \
.includes(:jobs) \
.where('jobs.active = true') \
.order('jobs.record_number asc, jobs.effective_date asc')
Here is the code I wish would work:
#employees_and_jobs = supervisor.direct_reports.alphabetical.includes(:jobs).active.sorted
The scopes direct_reports and alphabetical work, but the others (active and sorted) are interpreted as belonging to the Employee model, and give me an error. I want active and sorted to be interpreted as belonging to the Job model. How can I change the query to show that active and sorted are scopes for Job and not Employee?
The active and sorted scopes are of course defined on the Job model, and are done with an explicit reference to jobs (but of course that is not enough):
scope :sorted, -> { order('jobs.record_number asc, jobs.effective_date asc') }
scope :active, -> { where('jobs.active = true') }
(I didn't expect the explicit reference to jobs inside the scope to make it work, but I tried it just in case, and mention it in case someone else thinks it might work.)
How can I specify in my query that the final scopes are meant to apply to the included jobs, and not to the employees?
(I realize I can solve the problem with a default scope, but that can create new problems later, and I'm trying to avoid that. I would prefer the verbose version above over using a default scope.)
Similar (But Different) Questions
The answers to this question don't answer my question, but simply instead offer an alternative approach to dealing with the situation. (But I already have an alternative approach, given above. I have working code, but I'm trying to improve readability in a very particular way by using scopes on not just the main model but also the included model.)
I'm asking for a way to use scopes on the included model but those answers explain how to use a scope on the main model that in turn includes the other model. Two very different things. They are similar in that they both make the controller code simpler but the other approach makes the controller potentially less clear. It just moving all of the complexity into a single scope which would (in my case) be on the Employee model. I'm aiming to have have very specific scopes that I can compose together, which each have a very clear and clearly defined purpose.

scope is really just syntactic sugar for defining class methods. So like any other class method you can just call your scopes on the class which defines them:
class Job < ApplicationRecord
belongs_to :employee
scope :active, -> { where(active: true) }
scope :sorted, -> { order('jobs.record_number asc, jobs.effective_date asc') }
end
class Employee < ApplicationRecord
has_many :jobs
scope :with_active_jobs, ->{ include(:jobs).merge(Job.active).merge(Job.sorted) }
end
ActiveRecord::SpawnMethods#merge is probably one of the most underused features of AR. It lets you mash different scopes together programatically.
ActiveRecord is smart enough to specify the table with .where so there is not problem in using it in a join (.where('jobs.active = true') will also work fine too). Unfortunately .order is not as smart and .order(record_number: :asc, effective_date: :asc) will generate ORDER BY record_number ASC, effective_date ASC which will give an error.
There is no technical reason you have to do this in the model either. You can just do Employee.include(:jobs).merge(Job.active).merge(Job.sorted) or whatever in the controller if you want to compose the scopes there. But remember that controllers are really difficult to test compared to models.

Related

Deleting VS Finding Orphans using ActiveRecord helpers

I'm trying to delete all the organizations that no longer have any users.
Using the below code, I can find all the records I wish to delete:
Organization.includes(:users)
.where(users: { id: nil })
.references(:users)
When I add delete_all, I get the same error I would get if I didn't include references:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "users"
I could probably write the solution in pure SQL, but I don't understand why Rails isn't keeping the reference to users when I add the delete_all statement.
Here are some more details:
Organization:
has_many :users
User:
belongs_to :organization
I've found the includes useful only for eager loading (and it can rarely handle my cases), and when coupled with references it generates something completely insane (aliasing every single field with something like tN_rM) even though it actually does a LEFT OUTER JOIN... Which could help if it didn't vanish once delete_all appears!
I've found that it's much clearer and simpler just to use exists. It's Arel (and there's no point in avoiding it, its under the hood of ActiveRecord anyway), but it's such a tiny portion that it's barely noticeable:
Organization.where(
User.where('users.organization_id = organizations.id').exists.not
)
Or, if this string of SQL doesn't look nice to you, use a bit more Arel, so it gets noticeable:
Organization.where(
User.where(organization_id: Organization.arel_table[:id]).exists.not
) # I tend to extract these ^^^^^^^^^^^^^^^^^^^^^^^ into local variables
That handles chaining .delete_all on top just fine, since it's not (syntactically) a join, even though it's effectively equivalent to one.
The magic behind this
SQL has an EXISTS operator that is similar in functionality to a join, except for inability of selecting fields from a joined table. It forms a valid boolean expression which can be negated and thrown into WHERE-conditions.
In the "SQL-free" form I'm using an expression "column of a table", which turns out to be usable in Rails' hash-conditions. It's an accidental discovery, one of the few uses of Arel that does not make code too bulky.
I'm not sure how you plan to implement this in the MVC framework, but it seems clean to do the organization purge via model action. Whenever a user is deleted, check to see of the organization has any remaining members.
in the User.rb
class User < ActiveRecord::Base
before_destroy :close_user
...
def user_organization
Organization.where(user_id: id)
end
private
def close_user
unless user_organization.users.any?
user_organization.destroy
end
end
end
Added To apply callback delete solution to users being member of many organizations
If the user has multiple organizations
class User < ActiveRecord::Base
before_destroy :close_user
...
def user_organizations
Organization.where(user_id: id)
end
private
def close_user
user_organization.find_each do |organization|
unless organization.users.any?
organization.destroy
end
end
end
Caveat: this is not tested, didn't fail syntax. I don't have the data to test it fully but I think it will work. But it means running this action after every user delete, which is a system architecture decision. If it's an option, it might be worth a try.

Sortable UUIDs and overriding ActiveRecord::Base

I'm wanting to use UUIDs in an app I'm building and am running into a bit of a problem. Due to UUIDs (v4) not being sortable because they're randomly generated, I'm trying to override ActiveRecord::Base#first, but Rails isn't too pleased with that. It yells at me saying ArgumentError: You tried to define a scope named "first" on the model "Item", but Active Record already defined a class method with the same name. Do I have to use a different method if I want to sort and have it sort correctly?
Here's the sauce:
# lib/sortable_uuid.rb
module SortableUUID
def self.included(base)
base.class_eval do
scope :first, -> { order("created_at").first }
scope :last, -> { order("created_at DESC").first }
end
end
end
# app/models/item.rb
class Item < ActiveRecord::Base
include SortableUUID
end
Rails 4.2, Ruby 2.2.2
Reference:
http://blog.nakonieczny.it/posts/rails-support-for-uuid/
http://linhmtran168.github.io/blog/2014/03/17/postgres-uuid-in-rails/ ( Drawbacks section )
Rails 6 (currently in version 6.0.0rc1) comes to rescue with implicit_order_column!
To order by created_at and make .first, .last, .second etc. respect it is as simple as:
class ApplicationRecord < ActiveRecord::Base
self.implicit_order_column = :created_at
end
First of all, first and last aren't as simple as you seem to think they are: you're completely neglecting the limit argument that both of those methods support.
Secondly, scope is little more than a fancy way of adding class methods that are intended to return queries. Your scopes are abusing scope because they return single model instances rather than queries. You don't want to use scope at all, you're just trying to replace the first and last class methods so why don't you just override them? You'd need to override them properly though and that will require reading and understanding the Rails source so that you properly mimic what find_nth_with_limit does. You'd want to override second, third, ... and the rest of those silly methods while you're at it.
If you don't feel right about replace first and last (a good thing IMO), then you could add a default scope to order things as desired:
default_scope -> { order(:created_at) }
Of course, default scopes come with their own set of problems and sneaking things into the ORDER BY like this will probably force you into calling reorder any time you actually want to specify the ORDER BY; remember that multiple calls to order add new ordering conditions, they don't replace one that's already there.
Alternatively, if you're using Rails6+, you can use Markus's implicit_order_column solution to avoid all the problems that default scopes can cause.
I think you're going about this all wrong. Any time I see M.first I assume that something has been forgotten. Ordering things by id is pretty much useless so you should always manually specify the order you want before using methods like first and last.
After replacing id with uuid, I experienced some weirdness in the way associations were allocating foreign keys, and it wasn't that .last and .first, but instead because I simply forgot to add default: 'gen_random_uuid()' to one of the tables using a uuid. Once I fixed that, the problem was solved.
create_table :appointments, id: :uuid, default: 'gen_random_uuid()' do |t|

Rails scope / class method to select items where association is present

Can't seem to wrap my head around this problem. I have a message model below
Message
# content:string
# original_id:integer
# sender_id:integer
# receiver_id:integer
has_one :reply, class_name: "Message", foreign_key: "original_id"
belongs_to :original, class_name: "Message"
Each message can only have one reply and the reply message will have its corresponding original message.
What I'd like to do is create a scope or a class method that allows me to pull replied messages in one batch and unreplied messages in another.
Something like
# return messages that have a reply present
def self.replied
where(reply.present?)
end
# return messages that have no reply
def self.unreplied
where(reply.nil?)
end
so I can chain the methods and ultimately pull messages with
user1.messages.replied
It doesn't currently work because I can't use the where clause unless it's a DB column...so I was thinking about adding a "replied" boolean column into the DB so I could use the where clause but there's probably a solution to this that I'm just not thinking about. A scope with a lambda? I'm stuck right now.
Any help much appreciated
To find those that have been replied is fairly straightforward:
scope :replied, joins(:reply)
as anything without a reply will be filtered out with an INNER JOIN. To find those without replies is a bit more complex - you can either use a LEFT JOIN or an EXISTS subquery to accomplish this. includes is a simple way to force a LEFT JOIN:
scope :unreplied, includes(:reply).
where(replies_messages: {id: nil}).
where(original_id: nil)
An EXISTS subquery may be somewhat more efficient, but more complex to write (at this time), as it would involve invoking Arel tables (or Squeel). For most cases a LEFT JOIN would be 'good enough', and includes is a quick-and-dirty way to force the API to use one.

Does default scope get added on to other scopes in Rails 3.1?

Are the order queries in the following two scopes not needed because of the default?
scope :paid, where('is_paid_merchant = true').order('weekly_clicks DESC')
scope :unpaid, where('is_paid_merchant = false').order('weekly_clicks DESC')
default_scope order('weekly_clicks DESC')
If the default scope is indeed added onto all other scopes, what's the precedence of that order? Is it the first one applied or the last one?
Default scope is the first one applied. You can override it with other scopes, or use unscoped in order to avoid it completely. Personally, I find default scopes very bad practice since they make the code harder to understand.

What is scope/named_scope in rails?

I've recently started an internship. My employer uses ruby on rails, and I frequently encounter new syntax that I need to look up to understand. I've googled around for a good explanation of named_scope, but what I've found so far is mostly blog posts giving high praise for it, rather a straight definition or introduction.
What exactly is named_scope (now simply called scope) in ruby on rails?
A scope is a subset of a collection. Sounds complicated? It isn't. Imagine this:
You have Users. Now, some of those Users are subscribed to your newsletter. You marked those who receive a newsletter by adding a field to the Users Database (user.subscribed_to_newsletter = true). Naturally, you sometimes want to get those Users who are subscribed to your newsletter.
You could, of course, always do this:
User.where(subscribed_to_newsletter: true).each do #something
Instead of always writing this you could, however, do something like this.
#File: users.rb
class User < ActiveRecord::Base
scope :newsletter, where(subscribed_to_newsletter: true)
#yada yada
end
If you're using Rails 4 or newer, do this instead:
#File: users.rb
class User < ActiveRecord::Base
scope :newsletter, -> { where(subscribed_to_newsletter: true) }
#yada yada
end
This allows you to access your subscribers by simply doing this:
User.newsletter.each do #something
This is a very simple example but in general scopes can be very powerful tools to easy your work.
Check out this link: API Description
scope in active record is like class methods but they return Relation object which means you can call another scope or active record querying method on it.
For example, if you have a Zombie model (zombies table) with below mentioned scope methods,
class Zombie
scope :rotting, -> { where(rotting: true) }
scope :fresh, -> { where('age < ?', 25) }
scope :recent, -> { order(created_at: :desc) }
end
And you call
Zombie.rotting.fresh.recent.limit(3)
It translates to the below in SQL,
select "zombies.*" from "zombies" where "zombies"."rotting" = 't' and (age<20) order by create_at desc limit 3
Example above is based on rails 4 syntax
The best way to understand about the details is to go to API Documentation.
You'll get the complete details and the ways we can use Scopes.
API Documentation of Scope
Imagine you have a model: Person.
Now imagine you :
want all the people in the world who have red hair.
want all the people in the world who play cricket
You could get those particular classes of people by using a scope!
Person.red_hair.cricket ## finds all people with red hair who play cricket
Person.red_hair ## finds all people with red hair
Person.cricket ## finds all people who play cricket.
Now that wasn't so hard was it?

Resources