ActiveRecord PostgreSQL querying an Array - ruby-on-rails

I have setup a PostgreSQL array field identical to this example on edgeguides. I am querying on these fields like:
Book.where("'fantasy' = ANY (tags)")
But what I need is to query the inverse of this; all records where tags does not include 'fantasy' (in this example).
Anyone have any guidance? I cannot find much documentation on working with a PostgreSQL array field outside of the aforementioned guide.

You can negate your condition
Book.where("NOT('fantasy' = ANY (tags))")
So you can modify query to get records with NULL records also:
Book.where("NOT('fantasy' = ANY (tags)) or tags IS NULL")
Also you can run these queries in psql and check results
SELECT * FROM book WHERE NOT('fantasy' = ANY (tags));
SELECT * FROM book WHERE NOT('fantasy' = ANY (tags)) OR tags IS NULL;
SELECT * FROM book WHERE 'fantasy' = ANY (tags)
Maybe there is no records without tag 'fantasy'?

Try this:
Book.where.not('tags #> ARRAY[?]', "fantasy")

Related

Rails: How to remove n+1 query when we need to query association inside loop?

I have output as result in code having queries in it (only showing basic one here)
So basically I need sum of the custom line items as well as all line items
results = Order.includes(:customer, :line_items).where('completed_at IS NOT NULL')
results.each do |result|
custom_items_sum = result.line_items.where(line_item_type: 'custom').sum(:amount)
total_sum = result.line_items.sum(:amount)
end
In this code, there is n+1 query issue, I have tried adding includes but for sure it is not going to work as we have another query inside the loop, Any help will be appreciated??
If you don't want to trigger other queries in the loop you need to avoid methods which work on relations and use that ones which work on collections. Try
custom_items_sum = result.line_items.
select { |line_item| line_item.line_item_type == 'custom' }.
sum(&:amount)
This should work without n+1 queries.
Note that it's possible to write just one query and avoid this computation anyway but that's beyond the scope of your question :)
Rails was never known to be robust enough as ORM. Use plain SQL instead:
results =
Order.connection.execute <<-SQL
SELECT order.id, SUM(line_items.amount)
FROM orders
JOIN line_items
ON (line_items.order_id = orders.id)
WHERE orders.completed_at IS NOT NULL
GROUP BY orders.id
HAVING line_items.line_item_type = 'custom'
SQL
That way you’ll get all the intermediate sums in a single query, which is way faster than performing all the calculations in ruby.
Just because #AlekseiMatiushkin says write it in raw SQL let's do the same with rails
order_table = Order.arel_table
line_items_table = LineItem.arel_table
custom_items = Arel::Table.new(:custom_items)
Order.select(
order_table[Arel.star],
line_items_table[:amount].sum.as('total_sum'),
custom_items[:amount].sum.as('custom_items_sum')
).joins(
order_table.join(line_items_table).on(
line_items_table[:order_id].eq(order_table[:id])
).join(
Arel::Nodes::As.new(line_items_table,:custom_items),
Arel::Nodes::OuterJoin
).on(
custom_items[:order_id].eq(order_table[:id]).and(
custom_items[:line_item_type].eq('custom')
)
).join_sources
).where(
order_table[:completed_at].not_eq(nil)
).group(:id)
This will produce an ActiveRecord::Relation of Order objects with a virtual attributes of total_sum and custom_items_sum using the following query
SELECT
orders.*,
SUM(line_items.amount) AS total_sum,
SUM(custom_items.amount) As custom_items_sum
FROM
orders
INNER JOIN line_items ON line_items.order_id = orders.id
LEFT OUTER JOIN line_items AS custom_items ON custom_items.order_id = orders.id
AND custom_items.line_item_type = 'custom'
WHERE
orders.completed_at IS NOT NULL
GROUP BY
orders.id
This should handle the request in a single query by using 2 joins to aggregate the needed data.
Try to use the scoping block. The following code generates very clean SQL queries.
Order.includes(:line_items).where.not(completed_at: nil).scoping do
#custom_items_sum = Order.where(line_items: { line_item_type: 'custom' })
.sum(:amount)
#total_sum = Order.sum(:amount)
end
There's not that much documentation about the scoping block but it scopes your model to the ActiveRecord requests made before (here : where('completed IS NOT NULL') and with the :line_items included).
Hope this helps! :)

Multiplying Columns in Active Record?

Have the following sql query, how would I translate the multiplication of two columns in Active Record?
SELECT plan_name, plan_price, count(plan_id), plan_price*count(plan_id) AS totalrevenue
FROM leads
INNER JOIN plans p ON leads.plan_id = p.id
WHERE lead_status_id = 5
GROUP BY plan_name, plan_price;
I'd try something like this.
leads = Lead.joins(:plans).
where(lead_status_id: 5).
group(:plan_name, :plan_price).
select('plan_name, plan_price, count(plan_id), plan_price * count(plan_id) AS totalrevenue')
leads.each { |lead| puts lead.totalrevenue }
Note:
I don't know if it's joins(:plan) or joins(:plans), it depends on your associations
If you need to call even the plan_id count I'd give a name even to that field
A field like totalrevenue is not displaied if you print the entire object, because it's not part of it, it's a virtual attribute

Rails Globalize sort objects by translated column with fallbacks and will_paginate

I have some models that I want to display in a paginated list (with will_paginate) sorted by a column translated with Globalize.
The requirements say that it has to be possible to create objects in any language and if no translation for the current locale are available, to fall back to any other in a predefined order.
The problem I'm running into is that if I join the table on the translation table, and the translations are mixed, will_paginate fails because even with a distinct call, the #count is calculated wrong and #limit on the AR Relation doesn't work as expected.
For example:
I have an Exhibitor model with a corresponding Exhibitor::Translation from Globalize and the Exhibitor objects can have translations in any or all configured locales.
Exhibitor.with_translations.order(:sort_string).limit(2)
returns just one object, because the first Exhibitor object has 2 translations and even a #distinct call doesn't change this, which means will_paginate is getting confused and
I think what I want is something like this:
SELECT exhibitors.id,translations.sort_string
FROM exhibitors
INNER JOIN
exhibitor_translations translations ON translations.exhibitor_id =
exhibitors.id
WHERE translations.locale = 'en' OR translations.locale = 'de' OR
translations.locale = 'fr'
ORDER BY translations.sort_string;
The WHERE part is where I'm struggling, because I just want the first translation which exists, but here I'm getting back all available for each object.
I hope this is a somewhat understandable explanation, still trying to exactly formulate it in my head, so if any clarification is needed, please just ask.
I tried a couple variations of this solution here Globalize3 order records by translated attributes and taking fallbacks into consideration
but will_paginate still shows the wrong count and doesn't display the navigation links.
If anyone runs into the same problem, I finally solved it with a subquery like this:
SELECT DISTINCT
COALESCE(b.exhibitor_id, c.exhibitor_id, a.exhibitor_id) exhibitor_id,
COALESCE(b.sort_string, c.sort_string, a.sort_string) sort_string
FROM exhibitor_translations a
LEFT JOIN
(
SELECT ID, exhibitor_id, sort_string, locale
FROM exhibitor_translations
WHERE locale = 'fr'
) b ON a.exhibitor_id = b.exhibitor_id
LEFT JOIN
(
SELECT ID, exhibitor_id, sort_string, locale
FROM exhibitor_translations
WHERE locale = 'en'
) c ON a.exhibitor_id = c.exhibitor_id;
The Rails code looks like this (not the final production code but close enough):
entity = Exhibitor
query = entity.all
translation_table = entity.translations_table_name
foreign_key = "#{entity.name.demodulize.underscore}_id"
attribute_name = "sort_string"
subquery = entity.translation_class.select("
DISTINCT
COALESCE(b.id, a.id) id,
COALESCE(b.#{foreign_key}, a.#{foreign_key}) #{foreign_key},
COALESCE(b.#{attribute_name}, a.#{attribute_name}) #{attribute_name}
")
subquery.from("#{translation_table} AS a")
subquery = subquery.joins("
LEFT JOIN
(
SELECT id, #{foreign_key}, #{attribute_name}
FROM #{translation_table}
WHERE locale = '#{I18n.locale}'
) b ON a.id <> b.id AND a.#{foreign_key} = b.#{foreign_key}
")
query = query.joins("INNER JOIN (#{subquery.to_sql}) t ON #{entity.table_name}.id = t.#{foreign_key}")
query.order("t.#{attribute_name} ASC")
If anyone wants to use this, be mindful of SQL Injection possibilties and use ActiveRecord#quote for external values (I don't have any user inputs for this query at the moment)
For > 2 locales use multiple join clauses like in the raw sql query above.

Rails find_by_sql not working

I´m trying to do execute find_by_sql Rails method and it´s not working.
Query:
#boats = Boat.find_by_sql(["SELECT *
FROM boats b, ports p
WHERE p.name = ?
AND b.port_id = p.id", port])
puts #boats.inspect
Query returns two elements, however it´s the same element twice [id=1 and id=1]. If I run the query in the database then I got two different results [id=1 and id=2]
You seem to be selecting all columns of both the boats table and the ports table in order to create a set of Boat objects. Perhaps you mean to select b.*?

Get a Rails record count without quering a 2nd time

I've got a Rails ActiveRecord query that find all the records where the name is some token.
records = Market.where("lower(name) = ?", name.downcase );
rec = records.first;
count = records.count;
The server shows that the calls for .first and .count were BOTH hitting the database.
←[1m←[35mCACHE (0.0ms)←[0m SELECT "markets".* FROM "markets" WHERE (lower(nam
e) = 'my market') LIMIT 1
←[1m←[36mCACHE (0.0ms)←[0m ←[1mSELECT COUNT(*) FROM "markets" WHERE (lower(na
me) = 'my market')←[0m
Why is it going to the database to get the count when it can use the results already queried?
I'm concerned about future performance. Today there are 1000 records. When that table holds 8 million rows, doing two queries one for data, and one for count, it will be expensive.
How do I get the count from the collection, not the database?
RactiveRecord use lazy query to fetch data from database. If you want to simple count the records, you can only call size of the retrun array.
records = Market.where("lower(name) = ?", name.downcase ).all
records.size
So, records is an ActiveRelation. You would think it's an array of all your Market records that match your where criteria, but it's not. Each time you reference something like first or count on that relation, it performs the query retrieve what you're asking for.
To get the actual records into an array, just add .all to the relation to actually retrieve them. Like:
records = Market.where("lower(name) = ?", name.downcase).all
count = records.count
For Rails 6.0.1 and Ruby 2.6.5
You will need to store the results into an array by using the to_a.
records = Market.where("lower(name) = ?", name.downcase).to_a
This will create the SQL query and store the results in the array records.
Then, when you call either records.first or records.count it will only return the data or do the calculation, not rerun a query. This is the same for records.size and records.length.
Another Example
I was needing to do this for a blog I am developing. I was trying to run a query to find all of the tags associated with a post, and I wanted to count how many tags there were. This was causing multiple queries until I came across the to_a suffix.
So, my SQL query looks like this:
#tags = TagMap.where(post_id: #post).joins(:tag).select(:id, '"tags"."name"').to_a
This looks through my TagMap table for all records that have post_id equal to the id of the post that I am viewing. It then joins to the Tags table and pulls only the id of the TagMap record and the name of the tag from the Tags table. Then it puts them all into an array. I can then run #tags.count and it will return the number of TagMap records for that post without doing another query.
I hope that this helps anyone using Rails 6+

Resources