Scopes, lambda and rails 3 - ruby-on-rails

having a play with scopes tonight in rails 3, and trying to get my head around what lambda does?
What I am trying to achieve in this example is get a list of country names (:name) from my countries model which is associated with my recipe model. A recipe belongs_to country and a country has many recipes.
I would like to order the recipes by amount of times they appear in a recipe, starting with the highest..
So i am trying this in my recipe model ( or should I do it within the country model?, but then that wouldn’t work would it as my country model is pre populated with 1 instance of every country in the world)
scope :top_countries, lambda { joins(:countries).merge(Country.name).order("name DESC") }
however i get this error message
undefined method `default_scoped?' for "Country":String
my controller
#toprankingcountry = Recipe.top_countries
obviously my understanding is not what i thought and would appreciate some pointers/assistance
Thanks

I believe the problem is with merge(Country.name) part. joins(:countries) returns ActiveRecord::Relation instance. Its merge method expects one argument which is another instance of a ActievRecord::Relation, whereas you merge string Country.name.
Generally speaking, getting a list of top countries effectively means getting a list of Countries order by some additional condition. So I'd put this logic into a Country model.
class Country
has_many :recipes
def self.top_countries
joins(:recipes).
select('countries.*, count(*) AS recipes_count').
group('countries.id').
order('recipes_count DESC')
end
end
Also if you are using RDBMS which cannot figure out dependent rows on its own (like PostgreSQL < 9.1) you'll have to manually list all the columns in group by clause.
get a list of country names (:name) from my countries model which is associated with my recipe model (instance)
I believe it is impossible to achieve what you described as is because a country has_many recipes, not vice versa

Related

Optimizing queries in a has_many_through association with three models

Trying to avoid n+1 query
I'm working on a web based double entry accounting application that has the following basic models;
ruby
class Account < ApplicationRecord
has_many :splits
has_many :entries, through: :splits
end
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
attribute :amount, :integer
attribute :reconciled
end
class Split < ApplicationRecord
belongs_to :entry, inverse_of: :splits
belongs_to :account
attribute :debit, :integer
attribute :credit, :integer
attribute :transfer, :string
end
This is a fairly classic Accounting model, at least it is patterned after GnuCash, but it leads to somewhat complex queries. (From ancient history this is pretty much a 3rd normal form structure!)
First Account is a hierarchal tree structure (an Account belongs to a parent (except ROOT) and my have many children, children may also have many children, which I call a family). Most of these relations are covered in the Account model and optimized as much as you can a recursive structure.
An Account has many Entries(transactions) and entries must have at least two Splits that the sum of the Amount attribute(or Debits/Credits) must equal 0.
The primary use of this structure is to produce Ledgers, which is just a list of Entries and their associated Splits usually filtered by a date range. This is fairly simple if the account has no Family/Children
ruby
# self = a single Account
entries = self.entries.where(post_date:#bom..#eom).includes(:splits).order(:post_date,:numb)
It get more complex if you want a ledger of an account that has many children (I want a Ledger of all Current Assets)
ruby
def self.scoped_acct_range(family,range)
# family is a single account_id or array of account_ids
Entry.where(post_date:range).joins(:splits).
where(splits: {account_id:family}).
order(:post_date,:numb).distinct
end
While this works, I guess I have an n+1 query because if I use includes instead of joins I won't get all the splits for an Entry, only those in the family - I want all splits. That means it reloads(queries) the splits in the view. Also distinct is needed because a split could reference an account multiple time.
My question is there a better way to handle this three model query?
I threw together a few hacks, one going backwards from splits:
ruby
def self.scoped_split_acct_range(family,range)
# family is a single account_id or array of account_ids
# get filtered Entry ids
entry_ids = Split.where(account_id:family).
joins(:entry).
where(entries:{post_date:range}).
pluck(:entry_id).uniq
# use ids to get entries and eager loaded splits
Entry.where(id:eids).includes(:splits).order(:post_date,:numb)
end
This also works and by the ms reported in log, may even be faster. Normal use of either would be looking at 50 or so Entries for a month, but then you can filter a years worth of transactions - but you get what you asked for. For normal use, an ledger for a month is about 70ms, Even a quarter is around 100ms.
I've used a few attributes in both Splits and Accounts that got rid a few view level queries. Transfer is basically concatenated Account names going up the tree.
Again, just looking to see if I'm missing something and there is a better way.
Using a nested select is the proper option IMO.
You can optimize your code with the nested select to use the following:
entry_ids = Entry.where(post_date: range)
.joins(:splits)
.where(post_date: range, splits: { account_id: family })
.select('entries.id')
.distinct
Entry.where(id: entry_ids).includes(:splits).order(:post_date,:numb)
This will generate a single SQL statement with a nested select, instead of having 2 SQL queries: 1 to get the Entry ids and pass it to Rails and 1 other query to select entries based on those ids.
The following gem, developed by an ex-colleague, can help you deal with this kind of stuff: https://github.com/MaxLap/activerecord_where_assoc
In your case, it would enable you to do the following:
Entry.where_assoc_exists(:splits, account_id: 123)
.where(post_date: range)
.includes(:splits)
.order(:post_date, :numb)
Which does the same thing as I suggested but behind the scene.

Rails ActiveRecord/Arel query aggregate column with select()

I've got two basic models with a join table. I've added a scope to compute a count through the relation and expose it as an attribute/psuedo-column. Everything works fine, but I'd now like to query a subset of columns and include the count column, but I don't know how to reference it.
tldr; How can I include an aggregate such as a count in my Arel query while also selecting a subset of columns?
Models are Employer and Employee, joined through Job. Here's the relevant code from Employer:
class Employer < ApplicationRecord
belongs_to :user
has_many :jobs
has_many :employees, through: :jobs
scope :include_counts, -> do
left_outer_joins(:employees).
group("employers.id").
select("employers.*, count(employees.*) as employees_count")
end
end
This allows me to load an employer with counts:
employers = Employer.include_counts.where(id: 1)
And then reference the count:
count = employers[0].employees_count
I'm loading the record in my controller, which then renders it. I don't want to render more fields than I need to, though. Prior to adding the count, I could do this:
employers = Employer.where(id: 1).select(:id, :name)
When I add my include_counts scope, it basically ignores the select(). It doesn't fail, but it ends up including ALL the columns, because of this line in my scope:
select("employers.*, count(employees.*) as employees_count")
If I remove employers.* from the scope, then I don't get ANY columns in my result, with or without a select() clause.
I tried this:
employers = Employer.include_counts.where(id: 1).select(:id, :name, :employee_counts)
...but that produces the following SQL:
SELECT employers.*, count(employees.*) as employees_count, id, name, employees_count FROM
...and an SQL error because column employees_count doesn't exist and id and name are ambiguous.
The only thing that sort of works is this:
employers = Employer.include_counts.where(id: 1).select("employers.id, employers.name, count(employees.*) as employees_count")
...but that actually selects ALL the columns in employers, due to the scope clause again.
I also don't want that raw SQL leaking into my controller if I can avoid it. Is there a more idiomatic way to do this with Rails/Arel?
If I can't find another way to do the query, I'll probably create another scope or custom finder in the model, so that the controller code is cleaner. I'm open to suggestions for doing that as well, but I'd like to know if there's a simple way to reference computed aggregate columns like this as though they were any other column.

Ordering by a column in an associated model

I am trying to make an ActiveRecord query that will order the results by the value of one of the columns in an associated model:
I have a Chats model that has a one to many relationship with the messages model.
Chats has_many messages and Message belongs_to chat
And from my controller, I want to get a list of chats, ordered by the created_at of the associated message.first, eg:
#chats = current_user.chats.includes(:messages).order("messages.first.created_at").paginate(page: params[:page])
or something like that.
My question is that how can I achieve this kind of ordering from an associated model with this relationship? All contributions are appreciated.
Also add .references(:messages)
This will pull messages in as a join rather than a separate query.
You can define your order in your association. Try the following:
has_many messages, -> { order(:created_at => :asc) }
# change how you need
So when you call chat.messages it will return messages with the given order.
Thank you all, #Rubyrider and #Andrew. I have been able to order the columns as follow:
#chats = current_user.chats.includes(:messages).order("messages.created_at desc").paginate(page: params[:page])
without the inclusion of either .first or .last on messages. Surprisingly, I did not have to specify which of the messages of the chat is to be used for the ordering. I guess ActiveRecord automatically looks and just takes the latest of the associated model to use for the ordering.

How would you join two records that belong_to the same record without an N+1?

I've got an application in which businesses can file taxes and request tax extensions. (An extension is a request saying "I need more time to file.")
I have the following relationships:
Business has_many :tax_filings (one per year)
TaxFiling belongs_to :business
Business has_many :tax_extensions (one per year)
TaxExtension belongs_to :business
When I show a list of tax filings, I want each filing to show whether there is a corresponding extension for it. But I'm not sure how to do that without an N+1 query.
Right now I have this method on TaxFiling:
def extension
TaxExtension.where(:business_id => business_id, :year => year).first
end
So every time I call TaxFiling#extension, it does another database query.
I added a scope for TaxFiling that joins extensions on business_id and year, but I'm not sure how to get TaxFiling#extension to use that without having a declared relationship between the two models.
How can I do this?
I think what you want is the .includes method, in order to do eager loading, when you initially load the TaxFiling models. If you do something like this:
TaxFiling.includes(:business => [:tax_extensions])
Rails will load the associated businesses and extensions into memory, using three queries (one per model), instead of N queries.

Ruby on rails activerecord joins - select fields from multiple tables

models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages

Resources