Composing ActiveRecord scopes with selects - activerecord

Unfortunately, ActiveRecord's select replaces the existing SELECT clause instead of adding to it, so I can't compose queries. Does anyone have a workaround?
Example model:
class Story
scope :recent, -> { where("created_at >= ?", 1.month.ago) }
# deliberately simple examples, please don't get distracted memoizing, etc.
scope :with_net_score, -> { select("`stories`.*, (upvotes - downvotes) as net_score" }
scope :with_recent, -> { select("`stories.*, greatest(updated_at, last_vote_at) as recent") }
end
So while I can compose Story.recent.with_net_score, Story.with_net_score.with_recent fails. And both with_net_score and with_recent fail when Story comes after a join to an association.
How would you rewrite with_net_score so that it adds to the fields selected instead of replcaing them and can be composed with with_recent and joins?

Adding the author's own solution to this:
class ApplicationRecord < ActiveRecord::Base
scope :select_fix, -> { select(self.arel_table.project(Arel.star)) }
end
https://github.com/lobsters/lobsters/blob/master/app/models/application_record.rb

Related

Rails5: How can I use the ActiveRecord "OR" query with "includes"?

class Parent < ApplicationRecord
has_many :children
enum status: {
status1: 0,
status2: 1
}
end
class Child < ApplicationRecord
belongs_to :parent
end
# error
# "Relation passed to #or must be structurally compatible. Incompatible values: [:references]"
combination = Parent.status1.or(Parent.status2.includes(:children).where(children: {name: 'ABC'}))
I want to get the data "status1" or "status2 has children named 'ABC'", but error occurs.
The or method takes another relation that has a similar filter pattern, and combines it with the already-existing filters on the object being called.
For example, Parent.status1.or(Parent.status2) would give you a set of records that have either status: 1 or status: 2.
(In case someone is not familiar with it, the example in the question also uses enum, which allows filtering the enum's attribute value using the name of the value. #status1 and #status2 in this case correspond to { status: 0 } and {status: 1} respectively.)
In order to call more relation methods to modify the final result, you must call them on the result of calling #or, like this:
Parent.status1.or(Parent.status2).includes(:children).where(children: {name: 'ABC'})
Based on your comment I see now that you want records that either (have status1) or (have status2 and have a matching children record).
Note that in order to use a relation in a where (like where(children: { name: value }) you must join with the related table (joins(:children).where(children: { name: value }). It seems that ActiveRecord will infer the join if you use only includes, but that's not documented as far as I can tell. This is why or sees the two relations as incompatible: one has children in the references list, while the other does not.
If you write the where clause by hand as a string, it does not change the references list, so or does not see the relation as incompatible. When you write a where clause by hand, you must explicitly use joins:
Parent.status1.joins(:children).or(Parent.status2.joins(:children).where("children.name = 'ABC'"))
You are not calling "includes" on the final or result.
parent = Parent.status1.or(Parent.status2)
parent.includes(:chilren).where(children: {name: "ABC"})

Creating a scope on an ActiveRecord Model

I have a ActiveRecord model called Panda with a column called variant, which can consist of values like ‘bam-abc’, ‘bam-123’, ‘boo-abc’ and ‘boo-123’ I want to create a scope which selects all records where the variant starts with ‘boo’.
In the console, I can select those records (starting with ‘boo') with the following:
Panda.select{|p| p.variant.starts_with? 'boo'}
Is there a way to turn that into a scope on the Panda class? I need to be able to do a 'to_sql' on the scope for my RSpec tests.
You'd want to use a scope that sends a LIKE into the database, something like:
scope :boos, -> { where('pandas.variants like ?', 'boo%') }
or equivalently, use a class method:
def self.boos
where('pandas.variants like ?', 'boo%')
end
Then you can say things like:
Panda.boos.where(...)
Panda.where(...).boos
Panda.boos.where(...).to_sql
Panda.where(...).boos.to_sql
You only need to use the pandas prefix on the column name if you think you'll be doing JOINs with other tables that might leave the variant name ambiguous. If you'll never be doing JOINs or you'll only ever have one variant column in your database then you could use one of:
scope :boos, -> { where('variants like ?', 'boo%') }
def self.boos
where('variants like ?', 'boo%')
end
Add the line below to the Panda class
scope :boo, -> { where('variant LIKE ?', 'boo%') }
You can then use Panda.boo to get all the records with variant starting with boo. Panda.boo.to_sql will give you the sql

Grails many to many with 3 classes: sorting by the number of relationships

Let's say we have 3 domain classes: 2 classes related with each other through a 3rd class.
Ok, some code:
class A {
String subject
String description
static hasMany = [cs: C]
static transients = ['numberOfCs']
Long getNumberOfCs() {
return cs.size()
}
}
class B {
String title
}
class C {
A objectA
B objectB
static belongsTo = [a: A]
}
Pretty clear? I hope so. This work perfectly with my domain.
You can see the transient property numberOfCs, which is used to calculate the number of C instances related to my A object. And it works just fine.
The problem: listing all my A objects, I want to sort them by the number of relationships with C objects, but the transient property numberOfCs cannot be used for the scope.
How can I handle the situation? How can I tell GORM to sort the As list by numberOfCs as it would be a regular (non transient) field?
Thanks in advance.
I'm not sure that Grails' criteria do support this, as you need both to select the A object itself and aggregate by a child objects (C). That means grouping by all the A's fields, which is not done automatically.
If you only need some fields from A, you can group by them:
def instances = A.withCriteria {
projections {
groupProperty('subject')
count('cs', 'cCount')
}
order 'cCount'
}
otherwise you'll need to retrieve only ids and make a second query, like in this question.
Another way is to use derived properties like described here (not sure it will work though):
class A {
static mapping = {
numberOfCs formula: 'select count(*) from C where c.b_id = id'
}
}
I wouldn't consider your Problem GORM related but rather general Hibernate or even SQL related.
Take a look at the HQL Docu they are a lot of examples.
check the following HQL this pretty close what you are asking.
select mySortedAs from A mySortedAs left join mySortedAs.cs myCsOfA order by
count(myCsOfA)
I think I saw somewhere that you also can do something like this myCsOfA.length or myCsOfA.size

don't understand complex Ruby code

I'm currently reading Rails 3 In Action. There is code that I was wondering if someone could explain to me. I'm having a hard time understanding it:
scope :readable_by, lambda { |user| joins(:permissions).where(permissions: { action: "view", user_id: user.id })}
thanks,
mike
It's called a Rails scope. It essentially creates a class method called .readable_by(user) that does a SQL join on the permissions table and returns records where the action column value is "view" and the user_id column value equals user.id.
It could be used like so (assuming it's defined in the Comments model):
readable_comments = Comments.readable_by(current_user)
A simple scope that does nothing is this:
scope :my_scope_name, lambda {}
A scope that accepts a parameter is this:
scope :my_scope_name, lambda { |my_parameter| }
And then the above scope uses some ActiveRecord finder methods, specifically joins and where.

How to use Rails 3 scope to filter on habtm join table where the associated records don't exist?

I have an Author model which habtm :feeds. Using Rails 3 want to setup a scope that finds all authors that have no associated feeds.
class Author < ActiveRecord::Base
has_and_belongs_to_many :feeds
scope :without_feed, joins(:feeds).where("authors_feeds.feed_id is null")
end
...doesn't seem to work. It feels like a simple thing. What am I missing here?
To my knowledge ActiveRecord/Arel do not have a means of defining outer joins. So you'll have to write a bit more SQL than normal. Something like this should do the trick:
scope :without_feed, joins('left outer join authors_feeds on authors.id=authors_feeds.author_id').where('authors_feeds.feed_id is null')
I am of course guessing at your table names and foreign keys. But that should give you the picture.
In Rails >= 5, you can do it like this:
scope :without_feed, -> {
left_outer_joins(:feeds)
.where(authors_feeds: { author_id: nil })
}
scope :with_feed, -> {
left_outer_joins(:feeds)
.where.not(authors_feeds: { author_id: nil })
}

Resources