I have simple classes like these:
class Book
has_many :book_categorizations
has_many :categories, through: :book_categorizations, source: :book_category
end
class BookCategorizations
belongs_to :book
belongs_to :book_category
end
class BookCategory
has_many :book_categorizations
has_many :books, through: :book_categorizations
end
I would like to find Books that have no category. How can I query that using where?
You could add scope with an LEFT JOIN to your model:
# in book.rb
scope :without_categories, lambda {
joins('LEFT JOIN book_categorizations ON books.id = book_categorizations.book_id').
where(book_categorizations: { book_category_id: nil })
}
Which could be used like:
Book.without_categories
#=> returns books without a category
How it works:
Imaging you have a fruits and a colors table:
fruits
id | name
1 | Apple
2 | Orange
3 | Banana
colors
id | name
1 | black
2 | red
3 | yellow
And a colors_fruits join table:
colors_fruits
color_id | fruit_id
2 | 1 # red Apple
3 | 3 # yellow Banana
Since Rails' joins method generates INNER JOIN, all joins would only return fruits that have at least one color. The orange wouldn't be in the list, because it does not have a color (therefore no join is possible):
Fruit.joins(:colors)
#=> red Apple, yellow Banana (simplified)
But when we are interested into fruits that do not have an color, then we need an LEFT JOIN. A LEFT JOIN includes all elements from the left table - even if there is not matching on the right table (unfortunately there is no Rails helper for this kind of joins):
Fruits.joins('LEFT JOIN colors_fruits ON colors_fruits.fruits_id = fruits.id')
This generates a result like:
id | color | fruit_id | color_id
1 | Apple | NULL | NULL
2 | Orange | 2 | 1
3 | Banana | 3 | 3
Now we just need to exclude the ones that do not have a color_id
Fruits.joins('LEFT JOIN colors_fruits ON colors_fruits.fruits_id = fruits.id').
where(colors_fruits: { color_id: nil })
You might want to read about the different types of SQL JOINS. And there is this well known diagram about joins.
Related
I have these two models:
class ModelA < ApplicationRecord
has_one :model_b
has_one :model_b
end
class ModelB < ApplicationRecord
belongs_to :model_a
end
Data in DB tables:
model_a
id | ...
1 | ...
2 | ...
3 | ...
model_b
id | model_a_id | value_a | value_b
1 | 1 | abc | def
2 | 2 | ghi | jkl
For every record in the the model_a, I want to get a record from table model_b - I can get it like this.
ModelA.joins('LEFT JOIN model_b ON model_b.model_a_id = model_a.id')
This query would return me the rows with ID 1 and 2 from the table model_a. However, I would like to get returned also the row with ID 3 from the table model_a and for this row, I would want to get returned the associated (in this case, non-existing) row from model_b with these values:
value_a: NULL
value_b: NULL
How do I do that? I tried to play with different JOINS, with CASE IF/ELSE/END, but I happened to not find the right combination.
As I need to be able to filter/query these data, I believe it would be probably better to solve this on the PSQL level, rather than on Rails.
EDIT: RIGHT JOIN returns me only the first 2 rows form model_a.
EDIT2: This is the desired output:
modal_a.id | modal_b.value_a | modal_b.value_b
1 | abc | def
2 | ghi | jkl
3 | null | null
Thank you advance.
That's called a left outer join
ModelA.joins('LEFT OUTER JOIN model_b ON model_b.model_a_id = model_a.id')
It will return all ModelA records even if no modelB record is present.
In pure rails...
ModelA.includes(:model_b)
To explicitly include the columns that may have nil...
records = ModelA.includes(:model_b).select('*, model_b.value_a as model_b_value_a, model_b.value_b as model_b_value_b')
This lets you do records.first.id to see the model_a id, and records.first.model_b_value_a etc to see the value from model_b
For records without an associated model_b record, records.first.model_b_value_a will return nil
I'm trying to build a line chart with Chartkick that show the number sales by sellers in days, but I don't know how can I do this.
I'm already showing in a table the sellers name and your sales with this code:
#sales = Sale.where(created_at: DateTime.current.beginning_of_month..DateTime.current.end_of_month)
#sales_seller = #sales.joins(:user).select("user_id, COUNT(sales.id) AS total").group('user_id')
This returns:
+--------+-------+
| Name | Total |
+--------+-------+
| Jack | 10 |
| Kevin | 3 |
| Andrea | 11 |
+--------+-------+
How can I put the sales.created_at in this scenario?
My models
Sales
belongs_to :user
User (seller)
has_many :sales
This should work.
I used num_of_sales as the column in sales which contains the number of sales, you must fix it based on your Sale model:
data = #user.joins(:sales).select('users.name as name, sales.num_of_sales').group(:name).sum(:num_of_sales)
It is grouped by user name, but maybe it is better to group by user_id.
To filter sales, just chain a where method:
data = #user.joins(:sales).where(.....).select(.....
Then you can plot as line with:
<%= line_chart data %>
Summary:
I have a many to many relationship between attachments and rules, through alerts.
I have a given rule and a given selection of attachments (those with a given bug_id).
I need to go through all the selected attachments an indicate whether there is an alert for the rule or not, with a different CSS background-color.
Outer Join
I get the correct results with the following query:
SELECT attachments.*, alerts.rule_id
FROM attachments
LEFT OUTER JOIN alerts ON alerts.attachment_id = attachments.id
and alerts.rule_id = 9
WHERE attachments.bug_id;
I'm looking for something like:
bug.attachments
.left_joins(alerts: {'rules.id' => 9})
.select('attachments.*, alerts.rule_id')
Database
class Alert < ApplicationRecord
belongs_to :attachment
class Attachment < ApplicationRecord
has_many :alerts
attachments
| id | bug_id |
| 14612 | 38871 |
| 14613 | 38871 |
| 14614 | 38871 |
alerts
| attachment_id | rule_id |
| 14612 | 9 |
| 14614 | 8 |
Condition in the From Clause
Without the alerts.rule_id = 9 condition in the FROM clause, we get the following result:
| id | rule_id |
| 14612 | 9 |
| 14614 | 8 |
| 14613 | NULL |
So having a WHERE clause WHERE alerts.rule_id = 9 or alerts.rule_id is NULL would lose the result for 14612
So the following won't work:
bug.attachments
.joins(:alerts)
.select('attachments.*, alerts.rule_id')
.where( ??? )
Edit
The above is a simplified and corrected version of my original question.
The original question is below:
alerts belongs to rules and attachments, and attachments belong to bugs.
class Alert < ApplicationRecord
belongs_to :attachment
belongs_to :rule
class Attachment < ApplicationRecord
belongs_to :bug
has_many :alerts
class Bug < ApplicationRecord
has_many :attachments
For a given rule, I need to show all the attachments for a given bug, and whether there is an alert or not. I want the following SQL:
SELECT attachments.*, alerts.id as alert_id
FROM `attachments`
LEFT OUTER JOIN `alerts` ON `alerts`.`attachment_id` = `attachments`.`id`
LEFT OUTER JOIN `rules` ON `rules`.`id` = `alerts`.`rule_id` AND rules.id = 9
WHERE `attachments`.`bug_id` = 38871
I can get this from:
bug.attachments
.joins("LEFT OUTER JOIN `alerts` ON `alerts`.`attachment_id` = `attachments`.`id`")
.joins("LEFT OUTER JOIN `rules` ON `rules`.`id` = `alerts`.`rule_id` AND rules.id = 9")
.select('attachments.*, alerts.id as alert_id')
.map{|attach| [attach.file_name, attach.alert_id]}
What I want to know is how to avoid calling joins with a string SQL fragment.
I'm looking for something like:
bug.attachments
.left_joins(alerts: {rule: {'rules.id' => 9}})
.select('attachments.*, alerts.id as alert_id')
.map{|attach| [attach.file_name, attach.alert_id]}
Is there anyway to avoid passing an SQL string?
Actually I think you will able to get the right results by putting rules.id = 9 in where clause.
SELECT attachments.*, alerts.id as alert_id
FROM `attachments`
LEFT OUTER JOIN `alerts` ON `alerts`.`attachment_id` = `attachments`.`id`
LEFT OUTER JOIN `rules` ON `rules`.`id` = `alerts`.`rule_id`
WHERE `attachments`.`bug_id` = 38871 AND (rules.id = 9 OR rules.id IS NULL)
I have the models Post, Tag, and PostTag. A post has many tags through post tags. I want to find posts that are exclusively tagged with more than one tag.
has_many :post_tags
has_many :tags, through: :post_tags
For example, given this data set:
posts table
--------------------
id | title |
--------------------
1 | Carb overload |
2 | Heart burn |
3 | Nice n Light |
tags table
-------------
id | name |
-------------
1 | tomato |
2 | potato |
3 | basil |
4 | rice |
post_tags table
-----------------------
id | post_id | tag_id |
-----------------------
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 1 |
4 | 2 | 3 |
5 | 3 | 1 |
I want to find posts tagged with tomato AND basil. This should return only the "Heart burn" post (id 2). Likewise, if I query for posts tagged with tomato AND potato, it should return the "Carb overload" post (id 1).
I tried the following:
Post.joins(:tags).where(tags: { name: ['basil', 'tomato'] })
SQL
SELECT "posts".* FROM "posts"
INNER JOIN "post_tags" ON "post_tags"."post_id" = "posts"."id"
INNER JOIN "tags" ON "tags"."id" = "post_tags"."tag_id"
WHERE "tags"."name" IN ('basil', 'tomato')
This returns all three posts because all share the tag tomato. I also tried this:
Post.joins(:tags).where(tags: { name 'basil' }).where(tags: { name 'tomato' })
SQL
SELECT "posts".* FROM "posts"
INNER JOIN "post_tags" ON "post_tags"."post_id" = "posts"."id"
INNER JOIN "tags" ON "tags"."id" = "post_tags"."tag_id"
WHERE "tags"."name" = 'basil' AND "tags"."name" = 'tomato'
This returns no records.
How can I query for posts tagged with multiple tags?
You may want to review the possible ways to write this kind of query in this answer for applying conditions to multiple rows in a join. Here is one possible option for implementing your query in Rails using 1B, the sub-query approach...
Define a query in the PostTag model that will grab up the Post ID values for a given Tag name:
# PostTag.rb
def self.post_ids_for_tag(tag_name)
joins(:tag).where(tags: { name: tag_name }).select(:post_id)
end
Define a query in the Post model that will grab up the Post records for a given Tag name, using a sub-query structure:
# Post.rb
def self.for_tag(tag_name)
where("id IN (#{PostTag.post_ids_for_tag(tag_name).to_sql})")
end
Then you can use a query like this:
Post.for_tag("basil").for_tag("tomato")
Use method .includes, like this:
Item.where(xpto: "test")
.includes({:orders =>[:suppliers, :agents]}, :manufacturers)
Documentation to .includes here.
Using ruby on rails, I have a Customer table that I want to be able to add unlimited properties (key value pairs) to. I'm not sure what the key/value pairs will be yet so I'm not sure how to do this. For example, one customer could be:
Customer 1 properties:
color: 'yellow'
brand: 'nike'
sales: '33'
Customer 2 properties:
color: 'red'
phone_number: '1111111111'
purchases: '2'
Basically, customers can have any number of properties in a key/value pair.
How can I do this?
The "traditional" way to do this is with the Entity-Attribute-Value, or EAV pattern. As the name suggests, you'll create a new table with three columns: one for the "entity," which in this case is the Customer, one for the "attribute" name or key, and one for the value. So you'd have a table like this:
customer_properties
+----+-------------+--------------+------------+
| id | customer_id | key | value |
+----+-------------+--------------+------------+
| 1 | 1 | color | yellow |
| 2 | 1 | brand | nike |
| 3 | 1 | sales | 33 |
| 4 | 2 | color | red |
| 5 | 2 | phone_number | 1111111111 |
| 6 | 2 | purchases | 2 |
+----+-------------+--------------+------------+
You'll definitely want an INDEX on key and maybe on value (and customer_id, of course, but Rails will do that for you when you use relation or belongs_to in your migration).
Then in your models:
# customer.rb
class Customer < ActiveRecord::Base
has_many :customer_properties
end
# customer_property.rb
class CustomerProperty < ActiveRecord::Base
belongs_to :customer
end
This enables usage like this:
customer = Customer.joins(:customer_properties)
.includes(:customer_properties)
.where(customer_properties: { key: "brand", value: "nike" })
.first
customer.customer_properties.each_with_object({}) do |prop, hsh|
hsh[prop.key] = prop.val
end
# => { "color" => "yellow",
# "brand" => "nike",
# "sales" => "33" }
customer.customer_properties.create(key: "email", value: "foo#bar.com")
# => #<CustomerProperty id: 7, customer_id: 1, key: "email", ...>
As database design goes this is pretty solid, but as you can see it has some limitations: In particular, it's cumbersome. Also, you're restricted to a single value type (:string/VARCHAR is common). If you go this route you'll likely want to define some convenience methods on Customer to make accessing and updating properties less cumbersome. I'm guessing there are probably gems specifically for making the EAV pattern work nicely with ActiveRecord, but I don't know them off the top of my head and I hope you'll forgive me for not googling, since I'm mobile.
As Brad Werth points out, if you just need to store arbitrary properties and not query by them, serialize is a great alternative, and if you use PostgreSQL even the querying problem is surmountable thanks to its great hstore feature.
Good luck!
You may want to look into the hydra_attribute gem, which is an implementation of the Entity-Attribute-Value (EAV) pattern for ActiveRecord models.
You should be able to use serialize for this, and assign your properties hash to your properties attribute, and retrieve them in the same way.