There is a join table with three columns: id, product_a_id, product_b_id
class ProductConnection < ActiveRecord::Base
belongs_to :product_a, class_name: :Product
belongs_to :product_b, class_name: :Product
end
I would like to filter the table by a specific product regardless in which column the product id is contained. How can I write a named scope which respects that a product can be nil? The following draft is inspired by a post by Zack Holman though it does not work:
scope :find_by_product, \
lambda {|p| p.nil? ? { \
where(["product_a_id = ? OR product_b_id = ?", p.id, p.id]) : {} \
}
Then I would like to know how I can delete all products returned in the ActiveRecord::Relation?
It sounds like the issue is with how to make your find_by_product scope work when the passed in product is nil? I think your curly braces are a little mixed up. Either way, here's how I'd write it in case this helps:
ProductConnection
scope :for_product, ->(product) {
if product_id = product.try(:id)
where(arel_table[:product_a_id].eq(product_id).
or(arel_table[:product_b_id].eq(produt_id))
end
}
Then, once that works, you can just call destroy_all on the scope to destroy all the records.
ProductConnection.for_product(my_product).destroy_all
There is also delete_all, which you can use in the same way, if you really don't want the ActiveRecord callbacks included with destroy.
Related
Here is an example:
Let says I have a Student object which has a has_many relationship with ReportCard objects. ReportCard objects have a boolean field called "graded" that flags they have been graded. So it looks like:
class Student < ActiveRecord
has_many :report_cards
end
class ReportCard < ActiveRecord
# graded :boolean (comes from DB)
belongs_to :student
end
Now, let's say you want to create a default scope so that if a student has no graded ReportCards, you want to see all of them, but if they have at least one graded ReportCard, you only want to see the graded ones. Finally, let's say you order them by "semester_number".
Using this scope on ReportCard works correctly:
scope :only_graded_if_possible, ->(student) { where(graded: true, student: student).order(:semester_number).presence || order(:semester_number) }
But I want it to be the default scope for Student so I tried:
class Student < ActiveRecord
has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || order(:semester_number) }
end
but this does not work. It won't return any report_cards if there is a single graded report_card in the whole db. Looking at the queries that are run, first it runs something like:
SELECT report_cards.* FROM report_cards WHERE reports_cards.graded = t ORDER BY semester_number ASC
I think this must be the present? check part of the presence query and notice it does not filter on Student at all! So if there is a single report_card that is graded, the check passes and then it runs the following query to get what to return:
SELECT report_cards.* FROM reports_card WHERE report_card.student_id = 'student id here' AND report_card.graded = t ORDER BY semester_number
This query actually would be correct if the student had a graded report card but it is always empty if he does not.
I assume that possibly the filtering on Student is added afterwards. So I tried to somehow to get it to filter student right off the bat:
has_many :report_cards, ->{ where(graded: true, student: self).order(:semester_number).presence || order(:semester_number) }
This does not work either because it appears that "self" in this scope is not the Student object like I'd assume, but a list of all the report_card ids. Here is the query this one runs:
SELECT report_cards.* FROM report_cards WHERE report_cards.graded = t AND report_cards.student_id IN (SELECT report_cards.id FROM report_cards) ORDER BY semester_number ASC
That isn't even close to correct. How can I get this to work?
I think what it really comes down to is someone being able to pass "self" (meaning the current Student object) as a parameter into the scope being applied in the "has_many". Maybe that isn't possible.
You can pass object to has_many scope as a parameter to lambda
has_many :report_cards, -> (student) { ... }
Try this:
class Student < ActiveRecord::Base
has_many :report_cards, ->{ where(graded: true).order(:semester_number).presence || unscoped.order(:semester_number) }
end
I am using this in my project where I have a model which is associated with users:
has_many :users, -> { only_deleted }
And in the Users model create a scope of only_deleted, returning your users which are deleted.
There's a way to condition something to an associative table of ActiveRecord?
I retrieve segments this way:
#segments = Segment.all
But, a Segment has_many products. See:
models/product.rb:
class Product < ActiveRecord::Base
belongs_to :segment, dependent: :destroy
end
models/segment.rb:
class Segment < ActiveRecord::Base
has_many :products
end
The problem is: I just want to retrieve products whose its status is equals to 1. I can condition something like this using where on Segment model, but how can I achieve this for products?
What I already tried
I found a solution. Take a look:
#segments = Segment.find(:all, include: :products, conditions: {products: {status: 1}})
It worked, but I think the code can be better.
Why I think the code can be better
Well, why should I use include: :products if the association is already live within the models? We're associating things through the model and I'm sure that is something near to enough.
Ideas?
Segment.joins(:products).where("products.status = 1")
You can also use includes instead of joins. But rails will convert it into a join internally since you are using the products table attribute in the query
A few tips, that might help you.
For easy naming purposes, I am considering the status==1 as being active. Of course I have no idea what it means in your specific case.
class Product
ACTIVE=1
def self.active
where(status: ACTIVE)
end
end
Now you write something like:
segment.products.active
and this will return only the active products for the given segment.
The solution you found, which will retrieve all segments with (active) products, could be written differently as follows:
Segment.includes(:products).where(products: {status: 1})
Now, why so elaborate: this actually translates to a sql query, so you have to be a little more explicit about it.
If you only ever want those with a status of 1
class Segment < ActiveRecord::Base
has_many :products, :conditions => { :status => 1 }
end
In rails 3 or
class Segment < ActiveRecord::Base
has_many :products, -> { where status: 1 }
end
In rails 4
Obviously can use status: true if it's a boolean
Then
#segments = Segment.includes(:products)
The association has_many :products makes it possible to use include: :products in your scope. Therefore you shouldn't doubt in your solution. It is right, and it is just the same as solutions presented in the other answers but by other syntacsis.
This should do the job - and it's compatibile with AREL syntax:
#segments = Segment.joins(:products).where(products: {status: 1})
It's quite different that solution with include (or includes, as it would be Rails 3/4), because it generates query with INNER JOIN, while includes generates LEFT OUTER JOIN. Also, includes is usually used for eager loading associated records, not for queries with JOIN.
I'm trying to list all the user's products with a probable association where a flag 'notification' is set to zero.
user.probable_associations.where(:notified => 0).collect{|a| Product.where(:id => a.product_id).collect{|p| p.name}}.to_sentence
It seems like using a where and collect method twice within the statement isn't very good. Is there a better way to go about this?
Also, the result is something like
"[\"Product A\"] and [\"Product B\"]"
which is pretty ugly...and I still need to remove the extra punctuation "[\" \"]
instead of something clean like
"Product A and Product B"
EDIT based on Rich's Answer, still have issues because notified is a field in associations NOT product:
has_many :probable_associations, -> { where "associations.category = 3"}, class_name: 'Association', before_add: :set_probable_category
has_many :probable_products, class_name: 'Product', through: :probable_associations, source: :product do
def not_notified
select(:name).where(notified: 0)
end
end
I'd use an ActiveRecord Association extension:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :products do
def not_notified
select(:name).where(notified: 0)
end
end
end
#-> #user.products.not_notified
That's my contribution, but you could then use #spickermann & #tompave's controbutions and use .flatten.to_sentence
Without knowing what probable_associations does would I rewrite the code to something like this:
product_ids = user.probable_associations.where(:notified => 0).map(&:product_id)
Product.where(:id => product_ids).map(&:name).to_sentence
Assuming that probable_associations is just an ActiveRecord has_many association, and that you want to end up with a list of titles for Product records, you can use this:
ids = user.probable_associations
.where(notified: 0)
.pluck(:product_id)
result = Product.where(id: ids).pluck(:name).to_sentence
It's similar to #spikermann's answer, but pluck(:column_name) is faster than using a block and only extracts the required column from the DB.
Also, the reason your code produces that string is that, by the time you call to_sentence, you have an Array of sub-arrays. Each sub-array contains a single element: a product name.
That's because the second collect is sent to an ActiveRecord::Relation containing just one record.
You could have solved that problem with flatten, but the whole operation could just be refactored.
I have 3 models with "1 to n" associations, like this
Client --1 to n--> Category --1 to n--> Item
In one page, I need to display a list of Items along with their Categories. This page is subject to 3 level of filtering:
Client filtering: I know the client id (I'll use 'id=2' in this example)
Category name: dynamic filter set by the user
Item name: dynamic filter set by the user
And I'm getting more and more confused with ActiveRecord Associations stuff
In my ItemsController#index, I tried this:
categories = Client.find(2).categories
.where('name LIKE ?', "%#{params[:filter_categories]}%")
#items = categories.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
The second line raises a NoMethodError undefined method 'items' for ActiveRecord::Relation. I understand that the first line returns a Relation object, but I cannot find a way to continue from here and get the list of Items linked to this list of Categories.
I also started to extract the list of categories ids returned by the first line, in order to use them in the where clause of the second line, but while writing the code I found it inelegant and thought there may be a better way to do it. Any help would be very appreciated. Thanks
models/client.rb
class Client < ActiveRecord::Base
has_many :categories
has_many :items, through: :categories
...
end
models/category.rb
class Category < ActiveRecord::Base
belongs_to :client
has_many :items
...
end
model/item.rb
class Item < ActiveRecord::Base
belongs_to :category
has_one :client, through: :category
...
end
You can only call .items on a category object, not on a collection. This would work:
#items = categories.first.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
To get what you want, you can do the following:
#items = Item
.where('category_id IN (?) AND name LIKE ?', categories, "%#{params[:filter_items]}%")
Assuming that in the end you are only interested in what is in #items, it would be even better to do it in one query instead of two, using joins:
#items = Item.joins(:category)
.where('items.name LIKE ? AND categories.name = ? AND categories.client_id = 2', "%#{params[:filter_items]}%", "%#{params[:filter_categories]}%")
You can try smth like this:
item_ids = Client.find(2).categories.inject([]) { |ids, cat| ids |= cat.item_ids; ids }
items = Item.find(item_ids)
This how you can get a list of nested objects that associated through another table.
I have model Profile. Profile has_one User. User model has field email. When I call
Profile.some_scope.includes(:user)
it calls
SELECT users.* FROM users WHERE users.id IN (some ids)
But my User model has many fields that I am not using in rendering. Is it possible to load only emails from users? So, SQL should look like
SELECT users.email FROM users WHERE users.id IN (some ids)
Rails doesn't have the facility to pass the options for include query. But we can pass these params with the association declaration under the model.
For your scenario, you need to create a new association with users model under the profile model, like below
belongs_to :user_only_fetch_email, :select => "users.id, users.email", :class_name => "User"
I just created one more association but it points to User model only. So your query will be,
Profile.includes(:user_only_fetch_email)
or
Profile.includes(:user_only_fetch_email).find(some_profile_ids)
If you want to select specific attributes, you should use joins rather than includes.
From this asciicast:
the include option doesn’t really work with the select option as we don’t have control over how the first part of the SELECT statement is generated. If you need control over the fields in the SELECT then you should use joins over include.
Using joins:
Profile.some_scope.joins(:users).select("users.email")
You need extra belongs to in the model.
For simple association:
belongs_to :user_restricted, -> { select(:id, :email) }, class_name: 'User'
For Polymorphic association (for example, :commentable):
belongs_to :commentable_restricted, -> { select(:id, :title) }, polymorphic: true, foreign_type: :commentable_type, foreign_key: :commentable_id
You can choose whatever belongs_to name you want. For the examples given above, you can use them like Article.featured.includes(:user_restricted), Comment.recent.includes(:commentable_restricted) etc.
Rails does not support to select specific columns when includes. You know ,it's just lazy load.
It use the ActiveRecord::Associations::Preloader module to load the associated data before data actually using. By the method:
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact
if records.empty?
[]
else
records.uniq!
Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
end
end
preload_scope the third params of preload, is a way to select specify columns. But can't lazy load anymore.
At Rails 5.1.6
relation = Profile.where(id: [1,2,3])
user_columns = {:select=>[:updated_at, :id, :name]}
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(relation, :user, user_columns)
It will select the specify columns you passed in. But, it just for single association. You need create a patch for ActiveRecord::Associations::Preloader to support loading multiple complex associations at once.
Here is a example for patch
The way to use it, example
I wanted that functionality myself,so please use it.
Include this method in your class
#ACCEPTS args in string format "ASSOCIATION_NAME:COLUMN_NAME-COLUMN_NAME"
def self.includes_with_select(*m)
association_arr = []
m.each do |part|
parts = part.split(':')
association = parts[0].to_sym
select_columns = parts[1].split('-')
association_macro = (self.reflect_on_association(association).macro)
association_arr << association.to_sym
class_name = self.reflect_on_association(association).class_name
self.send(association_macro, association, -> {select *select_columns}, class_name: "#{class_name.to_sym}")
end
self.includes(*association_arr)
end
And you will be able to call like: Contract.includes_with_select('user:id-name-status', 'confirmation:confirmed-id'), and it will select those specified columns.
Using Mohanaj's example, you can do this:
belongs_to :user_only_fetch_email, -> { select [:id, :email] }, :class_name => "User"