Writing scope, join and order for a Model - ruby

My model is like this:
Program has_many Measures
and then
Measures has_many Targets
and Target table has a column named value
My query is like this:
#programs2 = Program.includes([measures: :targets])
.some_scope
.where('organization_id = 1')
.limit(2)
I don't know where or how to write the some_scope part of the query.
The query starts with Program.includes so I think it should be defined in the Program model but the problem I have is that measures: :targets . How do I define a join for them. If it was just one table I know I can do like this:
scope :salary, :joins => :registry, :order => "money DESC"
I need something similar for this one too, but this has has two tables like I explained above. I don't know how to write that one.

Try,
scope :some_scope, joins(mesures: :targets)

Related

Rails 5 ActiveRecord - combine OR adn AND clauses

I can't figure out the right syntax to use when including several models and using AND or OR clauses.
For example, there Shop model that has_one relation with Address model and belongs_to with Country.
How for example add OR to the below query:
Shop.includes(:address, :country)
Trying like this:
Shop.includes(:address, :country).where('countries.code'=> 'FR').and('counties.updated_at > ?', Date.today.days_ago(7))
raises the error:
NoMethodError: undefined method `and' for #<Shop::ActiveRecord_Relation:0x00007fb90d0ea3f8>
I found this thread at SO, but in this case, I have to repeat the same where clause before each OR statement? - looks not so DRY :(
What am I missing ?
Don't kick yourself... you don't need to use and at all, just string another where in:
Shop.includes(:address, :country).where('countries.code'=> 'FR').where('counties.updated_at > ?', Date.today.days_ago(7))
There is a better solution if you need to add multiple OR clause to AND clause. To get around it, there is arel_table method that can be used as follows.
Let's say we have the following models
Shop -> has_one :address
Shop -> belongs_to :country
and we would like to find all the shops by country code and address updated_at or country updated_at should be greater then a date you pass in:
some_date = Date.today
countries = Country.arel_table
addresses = Address.arel_table
# creating a predicate using arel tables
multi_table_predicate = countries[:updated_at].gt(some_date).or(addresses[:updated_at].gt(some_date))
# building the query
shops = Shop.includes(:address, :country).where(countries: {code: 'FR'}).where(multi_table_predicate)
This will execute a LEFT OUTER JOIN and here is where clause:
WHERE "countries"."code" = $1 AND ("countries"."updated_at" > '2019-03-12' OR "addresses"."updated_at" > '2019-03-12')
Sure, you can chain more tables and multiply OR conditions if you want.
Hope this helps.

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

Access an ActiveRelation in a view

I have two models with the appropriate foreign key created in the people table:
class Person < ActiveRecord::Base
belongs_to :family
class Family < ActiveRecord::Base
has_many :people
If I do the following I get an object - #family_members - as an instance variable and I have no problems:
#family_members = Family.find(1)
I can access the 'child' people table fields easily in my view:
#family_members.people.first_name
However, if I use the arel way with "where" etc. I get an "ActiveRecord::Relation", not a normal object, which leaves me stumped as to how to access the same "first_name" field form the people table like I accessed above:
#family_members = Family.where(:id => 1)
or even
#family_members = Family.joins(:people).where(:id => 1)
(is the "joins" even required??)
I understand that using ".first" will cause the query to run:
#family_members = Family.where(:id => 1).first
But it returns an array, not an object, so if I use in my view:
#family_members.people.first_name
I get a "method 'people' unknown" error.
How can I access the 'first_name' field of the people table like I did with the object created by "find" but using an ActiveRecord relation?
* added information 7/15 ********
To clarify what I am looking for -- here is what I would have written if I were writing SQL instead of Arel:
SELECT f.home_phone, f.address, p.first_name, p.last_name, p.birthday
FROM families f INNER JOIN people p ON p.family.id = f.id WHERE family_id = 1
With that query's results loaded into a result set I could access:
myResultSet("home_phone") -- the home_phone from the families table
myResultSet("address") -- the address from the families table
myResultSet("first_name") -- the first_name from the people table
myResultSet("birthdate") -- the birthdate from the people table
If the two tables in the query have a same-named field I would just use "AS" to request one of the fields by another name.
I have used this kind of query/result set for many years in web apps and I am trying to deduce how to do the same in Rails and ActiveRecord.
#family_members.people.first_name shouldn't ever work so I'm surprised you find it working ... #family_members contains a Family object, #family_members.people is an array of Person objects.
The fact that you're calling it #family_members seems to make me think you're expecting it to be an array of Persons... in which case the correct code would be...
#family_members = Family.find(1).people # finds people in first Family object
If you expect #family_members to contain just the first family member, then...
#family_members = Family.find(1).people.first
If you want an array of first names of all family members, then...
#family_members = Family.find(1).people # finds people in 1st Family object
#family_members.map {|member| member.first_name} # array of first_name
#family_members = Family.find(1) and #family_members = Family.where(:id => 1) are functionally identical.. both retrieve the first Family object in the database in each case may contain zero, one, or multiple people.
Just to be clear, the "1" in all examples above refer to which Family object is retrieved, not which Person in the Family.

Rails 3 Query: How to get most viewed products/articles/whatever?

I always wondered how to query and get results that doesn't fit in a model. Similar how it's done using LINQ and projecting into anonymous objects.
So here's the simple schema:
# Product.rb
class Product < ActiveRecord::Base
has_many :product_views
# attributes: id, name, description, created_at, updated_at
end
# ProductView.rb
class ProductView < ActiveRecord::Base
belongs_to :product
# attributes: id, product_id, request_ip, created_at, updated_at
end
Basically I need to get a list of Products (preferably just id and name) along with the count of views it had. Obviously ordered by view count desc.
This is the SQL I want to get:
select
p.id,
p.name,
count(pv.product_id) as views
from
product_views pv
inner join
products p on pv.product_id = p.id
group by
pv.product_id
order by
count(product_id) desc
I tried the following and similar, but I'm getting ProductView objects, and I would like to get just an array or whatever.
ProductView.includes(:product)
.group('product_id')
.select("products.id, products.name, count(product_id)")
This kind of thing are trivial using plain SQL or LINQ, but I find myself stucked with this kind of queries in Rails. Maybe I'm not thinking in the famous 'rails way', maybe I'm missing something obvious.
So how do you do this kind of queries in Rails 3, and specifically this one? Any suggestions to improve the way I'm doing this are welcome.
Thank you
You can use Arel to do what you're looking for:
products = Product.arel_table
product_views = ProductView.arel_table
# expanded for readability:
sql = products.join(product_views)
.on(product_views[:product_id].eq(product[:id]))
.group(product_views[:product_id])
.order('views DESC')
.project(products[:id],
products[:name],
product_views[:id].count.as('views'))
products_with_views = Product.connection.select_all(sql.to_sql) # or select_rows to just get the values
Yes, it is long, but Arel is a very smart way to deal with creating complex queries that can be reused regardless of the database type.
Within a class method in the Product class:
Product.includes(:product_views).all.map { |p| [p.id, p.name, p.product_views.size] }
Then sort it however you want.
I don't know if there's a way to do it using your models. I would probably resort to:
Product.connection.select_rows(sql)
Which will give you an array of arrays. You can use select_all if you'd rather have an array of hashes.
Try this:
#product = Product.find(#product_id)
#product_views = #product.product_views.count
(Source - http://ar.rubyonrails.org/classes/ActiveRecord/Calculations/ClassMethods.html#M000292)
Hope this helps!

ActiveRecord: Find through multiple instances

Say I have the following in my controller:
#category1
#category2
and I want to find all stores associated with those two categories...
#stores = #category1.stores + #category2.stores
this does work, but unfortunately returns an unaltered Array, rather than a AR::Base Array, and as such, I can't do things like pagination, scope, etc...
It seems to me like there's a built-in way of finding through multiple instance association... isn't there?
##stores = #category1.stores + #category2.stores
#if you want to call API methods you can just add conditions with the category id
#stores = Store.find(:all, :conditions => ['category_id=?', a || b])
With ActiveRecord, whenever you're finding a set of unique model objects, calling find on that model is usually your best bet.
Then all you need to do is constrain the join table with the categories you care about.
#stores = Store.all(:joins => :categories,
:conditions => ['category_stores.category_id in (?)', [#category1.id, #category2.id]])

Resources