Pull Activerecord join table information through controller query - ruby-on-rails

I have a query that generated a list of product data; I am working to dig in a bit deeper and extract an attribute from a join table (ProductSelections). Both RFIDTag and Products have many of the Product Selections model (i.e. the schema table contains both an rfid_tag_id and a product_id). Since I'm working from the product list, I'm deriving my query from here:
#original query (works as needed):
#warehoused_products = Product.includes(:rfid_tags).joins(:rfid_tags).where("rfid_tags.location_type = 'Warehouse' AND rfid_tags.location_id = ?", params[:id])
#Attempt at getting product selection data:
#product_histories = #warehoused_products.joins("INNER JOIN product_selections ON :product_selections.product_id = products.id").includes(:product_selections)
This raises a fairly large postgreSQL error when I check it:
raise #product_histories.inspect:
PG::SyntaxError: ERROR: syntax error at or near ":" LINE 1: ...eted_at" IS NULL INNER JOIN product_selections ON :product_s...
On adjusting the query:
raise #warehoused_products.includes(:product_selections).joins(:product_selections).where("product_selections.product_id = ?", params[:product_id]).first.inspect
I get a a nil result - error-free, and likely cleaner, but not quite there.
A Product Selection object looks like so, for reference:
#<ProductSelection id: 269, user_id: 2, listing_id: 11, product_id: 35, created_at: "2016-07-14 15:38:11", updated_at: "2016-08-08 21:10:13", rfid_tag_id: 575, part_id: nil, use_time: 2173274, account_id: 1, deleted_at: nil>
How can I resolve the query so that I can pull the associated table object?

#warehoused_products = Product.includes(:rfid_tags).where("rfid_tags.location_type = ? AND rfid_tags.location_id = ?", 'Warehouse', params[:id])
#product_histories = #warehoused_products.includes("product_selections").where("product_selections.product_id = ?", params[:product_id]).first.inspect

Related

How can I find each ascendent of a record in a self-referential table without N+1

I am working with a self-referential model (Category) in a Ruby on Rails app, which has the following columns:
id, name, level, parent_id
belongs_to :parent, class_name: 'Category', optional: true
The concept is that a level 1 category can have a level 2 subcategory, which can have a level 3 subcategory, etc.
For example:
Category id: 1, name: 'Dessert', level: 1, parent_id: nil
Category id: 2, name: 'Cold', level: 2, parent_id: 1
Category id: 3, name: 'Cake', level: 3, parent_id: 2
Category id: 4, name: 'Ice Cream', level: 3, parent_id: 2
Category id: 5, name: 'Sponge', level: 4, parent_id: 3
I'd like to find each ascendent of a record, regardless of how many levels deep it is. I then want to concatenate all the names, in ascending order, into one string.
i.e., if I'm starting with Sponge, I'd like a method which returns "Dessert - Cold - Cake - Sponge"
What I have so far works but is an n+1 and doesn't feel very Railsy:
def self.concatenate_categories(order)
category = order.category
categories_array = []
order.category.level.times do
categories_array.push category.name
category = Category.find(category.parent_id) if category.parent_id.present?
end
categories_array.reverse.join(' - ')
end
If this order is for Sponge, I get "Dessert - Cold - Cake - Sponge".
If the order is for Cake, I get "Dessert - Cold - Cake".
You can try a recursive CTE to get each category parent based on its parent_id:
WITH bar AS (
WITH RECURSIVE foo AS (
SELECT
categories.id,
categories.name,
categories.parent_id
FROM categories
WHERE categories.id = 5
UNION
SELECT
p.id,
p.name,
p.parent_id
FROM categories p
INNER JOIN foo f
ON f.parent_id = p.id
) SELECT name FROM foo ORDER BY id
) SELECT STRING_AGG(name, ' - ') FROM bar
How about this? I haven't tested that code, but you get the idea, join as many times as there are levels and query once. Depending on how many levels there are, your solution can be faster than too many joins.
def self.concatenate_categories(order)
scope = order.category
categories_array = if category.level > 1
scope = scope.select('categories.name')
order.category.level.downto(1) do |l|
scope = scope.joins("JOIN categories as c#{l} ON categories.id = c#{l}.parent_id")
.select("c#{l}.name")
end
scope.to_a
else
Array.wrap(scope.name)
end
categories_array.reverse.join(' - ')
end

Is it better to use select or loop through an array of records in Rails?

My goal is to write the query below in the cleanest, most efficient way possible and minimize hitting the DB. Appreciate any guidance in advance.
I have retrieved some records that belong to a user, like below:
english_shows = #user.shows.where(language: 'english')
Let's say the shows belong to different categories (using a foreign key), so it looks like below:
<ActiveRecord::Relation [
<Show id: 1, category_id: 1, title: 'Rick and Morty'>,
<Show id: 2, category_id: 2, title: 'Black Mirror'>,
<Show id: 3, category_id: 3, title: 'Stranger Things'>,
<Show id: 4, category_id: 3, title: 'Game of Thrones'>,
...
]
If I want to get the titles of the shows for each category, I know I can use select like this. The same thing can be done with where, but this would cause an additional DB call. ([Edit] Actually, both would hit the DB twice).
# Using select
cartoons = english_shows.select { |show| show.category_id == Category.find_by(name: 'cartoon').id}.pluck(:title)
# Using where
cartoons = english_shows.where(category_id: Category.find_by(name: 'cartoon').id)pluck(:title)
However, the select method would still result in multiple lines of long code (in my actual use case I have more category types). Is it cleaner to loop through the records like this (taken from this SO answer)?
cartoons, science_fiction, fantasy = [], [], []
#cartoon_id = Category.find_by(name: 'cartoon')
#science_fiction_id = Category.find_by(name: 'cartoon')
#fantasy_id = Category.find_by(name: 'cartoon')
english_shows.each do |show|
cartoons << show if show.category_id == #cartoon_id
science_fiction << show if show.category_id == #science_fiction_id
fantasy << show if show.category_id == #fantasy_id
end
Try this:
english_shows
.joins(:category)
.select('shows.*, categories.name as category')
.group_by(&:category)

Input custom value from parameters to Rails SQL Query

I have written so code to query lists of users who go jogging at a certain time and in certain locations, but I am getting the following error: ActiveRecord::StatementInvalid: SQLite3::SQLException: near "'jogging'"
Does this mean I cannot write a string into that variable? Are there any solutions to this?
users = User.where('ids in (:user_ids)', user_ids:
Activity.where('title :title AND created_at >= :created_at AND location_id: location_id',
{title: 'jogging', created_at: Time.zone.now.beginning_of_day, location_id:
Location.where('id in id', id: user.activities.where.not(location_id: nil)
.order('created_at DESC').first)}))
You can simplified your query this way
location_id = Location.where(
id: user.activities
.where.not(location_id: nil)
.order('created_at DESC').first
)
user_ids = Activity.where("title = ? AND created_at > ? AND location_id = ?",
"jogging", Time.zone.now.beginning_of_day, location_id)
users = User.where(id: user_ids)
But If you want to keep query one liner. You can use this
User.where('id IN(:user_ids)',
user_ids: Activity.where('title = :title AND created_at >= :created_at AND location_id = :location_id', title: 'jogging', created_at: Time.zone.now.beginning_of_day,
location_id: Location.where('id IN(:id)', id: user.activities.where.not(location_id: nil)
.order('created_at DESC').first).ids)
)
Note: I am assuming that you have user object to use above one liner query
Hope this will help you.
I think you forgot to add an = in the query:
Your query:
Activity.where('title :title ...
What you want:
Activity.where('title = :title ...
And if you don't need an operator like > or <, you can also use:
Activity.where(title: title)
If you then need to chain it, it's pretty simple:
Activity.where(title: title).where('foo < ?', 100)

How can I do something like this with ruby arrays

I have an user array like this:
users_array = [[1,text for 1],[2,text for 2],[3,text for 3],[4,text for 4],[5,text for 5]]
here first element is user_id and second element is text which is specific to user_id in the same array.
Now I am trying to have user object from instead of ids in array like these.
users_array = [[#<User id: 1, encrypted_email: "">,text for 1],[#<User id: 2, encrypted_email: "">,text for 2],[#<User id: 3, encrypted_email: "">,text for 3],[#<User id: 4, encrypted_email: "">,text for 4],[#<User id: 5, encrypted_email: "">,text for 5]]
I am trying not to loop the array and hit the db thousand times for thousands user.
data = users_array.to_h
# find all users with single query and build your map
User.where(id: data.keys).map { |user| [user, data[user.id]] }
You could use transpose to extract ids and values, and zip to combine users and values:
ids, values = users_array.transpose
users_array = User.find(ids).zip(values)

Count the records where a field doesn't equal value X

I'm trying to perform a count on results where a field is not equal to a specific value, but it always fails to returns results.
For instance, assuming Company and Products have a one to many relationship, I might get the following array back from ActiveRecord by querying for Company.find(63).products (which would be the SQL equivalent of SELECT "products".* FROM "products" WHERE "products"."company_id" = 63;):
<Product id: 1, company_id: 63, foo_id: 1>,
<Product id: 2, company_id: 63, foo_id: 3>,
<Product id: 3, company_id: 63, foo_id: nil>,
<Product id: 4, company_id: 63, foo_id: nil>
However, if I try to extend the above query to count everything but the first record with the following:
Company.find(63).products.where("foo_id != ?", 1).count
Which in SQL is:
SELECT COUNT(*) FROM "products" WHERE "products"."company_id" = 63 AND (foo_id != 1)
I always seem to get 1 back as a result when I expect to see 3. Why is this happening and how can I get it to count correctly?
If it is like you say you should get 1 instead of 0. What you are looking for is DISTINCT FROM as = and != on null types both return unknown as the result.
SELECT COUNT(*) FROM
products
WHERE products.company_id = 63 AND (foo_id IS DISTINCT FROM 1)
http://sqlfiddle.com/#!1/8b7a0/3
And as for further information. DISTINCT FROM is PostgreSQL exclusive so the standard version of doing this would be:
SELECT COUNT(*) FROM
products
WHERE products.company_id = 63 AND (foo_id<>1 OR foo_id IS NULL)
have you tried using <> instaed of !=
<>
Company.find(63).products.where("foo_id != ?", 1).count
should be
Company.find(63).products.where("foo_id <> ?", 1).count
but you should take it one step further:
class Product
def self.not_foo(foo_id)
where("foo_id <> ?", foo_id)
end
end
now
Company.find(63).products.not_foo(1).count

Resources