I have three models that have a polymorphic relationship as follows:
class DataSource < ActiveRecord::Base
belongs_to :sourceable, polymorphic: true
end
class Foo < ActiveRecord::Base
has_many :data_sources, as: :sourceable
# For the sake of this example, I have place the scope here.
# But I am going to put it in a Concern since Baz needs this scope as well.
scope :bar_source_id, -> (id) do
joins(:data_sources)
.where(data_sources: { source: 'bar', source_id: id })
.first
end
end
class Baz < ActiveRecord::Base
has_many :data_sources, as: :sourceable
end
I want to be able to find a Foo record based on the source_id of the related DataSource. In otherwords, Foo.bar_source_id(1) should return a Foo record who's data_sources contains a record with source: 'bar', source_id: 1. However the scope on the Foo model does not work as I expected.
Foo.bar_source_id(1) returns the correct Foo record but if there is no DataSource with a source_id of 1 Foo.all is returned.
On the other hand Foo.joins(:data_sources).where(data_sources: { source: 'bar', source_id: 1 }).first will always return either the correct record or nil if no record exists. This is the behaviour I expected.
Why does this query work when I call it off of the model itself, but not when I include it in the model as a scope?
Edit:
I have a partial answer to my question. .first in the scope was causing a second query that loads all Foo records. If I remove .first I will get back an ActiveRecord_Relation.
Why does .first behave differently in these two contexts?
After digging around I found this: ActiveRecord - "first" method in "scope" returns more than one record
I assume the rationale here is that scope isn't meant to return a single record, but a collection of records.
I can get the results I need by adding a method bar_source_id instead of using a scope.
def self.bar_source_id(id)
joins(:data_sources)
.where(data_sources: { source: 'bar', source_id: id })
.first
end
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.
Before a :through relationship has been persisted, is it still possible to return the association?
Here is a simplified version of the class structure:
class Foo < ActiveRecord::Base
has_many :quxes
has_many :bars, through: :quxes
end
class Bar < ActiveRecord::Base
# there is no has_many :quxes -- this class doesn't care
end
class Qux < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
So, I want to make calls like foo.bars which is equivalent to foo.quxes.map(&:bars)
I'm cloning Foos, but not saving them. Quxs are copied from old_foo to new_foo by
new_foo.quxes << old_foo.quxes.map(&:dup)
Please note that the above results in:
new_foo.quxes.first.foo == new_foo
new_foo.quxes.first.foo_id == old_foo.id
which shows that the association exists, but is not yet persisted.
It seems to me you should now be able to do:
new_foo.bars # same as new_foo.quxes.map(&:bar)
But it actually returns []
Is it possible for this association new_foo.bars to work before new_foo and its new quxes are saved? Is this even expected/desirable behavior for :through?
The through relation still "works" in the sense that you can manipulate it normally. I think what you mean is that it doesn't contain any bars added to any of the quxes. This is because the relation bars is separate from the relation quxes even if its not independent. To put it another way, bars is not simply quxes.map(&:bar), as you say; it runs a totally separate query, something like:
> puts foo.bars.to_sql
SELECT "bars".* FROM "bars" INNER JOIN "quxes" ON "bars"."id" = "quxes"."bar_id" WHERE "quxes"."foo_id" = 1
> puts new_foo.quxes.to_sql
SELECT "quxes".* FROM "quxes" WHERE "quxes"."foo_id" = 1
This means that unless an associated bar is persisted, the bars relation's SQL won't pick it up:
> persisted_foo.quxes.first.build_bar
#<Bar id: nil>
> persisted_foo.bars
#<ActiveRecord::Associations::CollectionProxy []>
> persisted_foo.quxes.first.save!
> persisted_foo.reload
> persisted_foo.bars
#<ActiveRecord::Associations::CollectionProxy [#<Bar id: 1>]>
Rails beginner here:
I already have a database and table, so the naming convention is giving me some headaches
class Item < ActiveRecord::Base
belongs_to :categorie, :foreign_key => "catid"
end
class Categorie < ActiveRecord ...
has_many :item
end
i = Item.first # Ok
c = i.Categorie # Ok, finds proper Categorie based on "catid" of i
c.Item # fails with Categorie_id column not found ! how can i map Categorie_id to "catid"?
You're a rails beginner but you might not be a programmer beginner so I'll dive in and explain classes a little bit.
A class is simply a data object that holds methods. Nothing more. Here's a simple one that holds one method:
class Cow
def talk
"moo"
end
end
Cow is the class, talk is the method. Now, if we have the above classes in memory, we cannot do this in the console:
talk
Because that method isn't available at the global scope. This is a good thing, because this could cause bugs and is inefficient. Imagine if we have a few animals:
class Cat
def talk
"meow"
end
end
class Dog
def talk
"woof"
end
end
Running talk, how would the computer know which talk to run? Instead, we call the method that's inside the class like this:
Cow.talk #=> "moo"
Cat.talk #=> "meow"
Dog.talk #=> "woof"
Hopefully now, this code:
Item.first
is less cryptic. Item is a class, and first is a method available inside that class.
Now I know Item is a model, but in rails, models are simply classes that inherit a bunch of useful methods from ActiveRecord. At the top of the Item model you should see this:
class Item < ActiveRecord::Base
That's what pulls in all of the useful methods, such as the first method we're using. Because of this inheritance, we can imagine your Item class looks a bit like this:
class Item < ActiveRecord::Base
def first
# code is in here that queries the table in your database that has
# the downcased and pluralized name of Item (so items) and returns the first
# row of that table
end
# down here is all of your methods you've probably created. Validations and the like.
end
first, rather than return a string like in my example does something far more useful; it queries the table in your database that has the downcased and pluralized name of its class. So Item.first queries the items table, and returns the first row.
Now, I have to be honest, despite what you say, I find it highly doubtful that i.Categorie finds the proper Categorie based on the "catid" of i. If it truly does I feel you've done some crazy workaround to get that working. This is what should happen:
i.Categorie
NoMethodError: undefined method `Categorie' for #<Item:0x00000005905830>
In plain English, this means
NoMethodError: there is no 'Categorie' method inside that instance of the 'Item' class.
And this makes sense because I see no 'Categorie' method in here:
class Item < ActiveRecord::Base
def first
# code is in here that queries the table in your database that has
# the downcased and pluralized name of Item (so items) and returns the first
# row of that table
end
# down here is all of your methods you've probably created. Validations and the like.
end
Now the reason c.Item doesn't work is because c is set to nil because nil was returned by i.Categorie due to the non-method error, and nil certainly doesn't have the method Item inside it.
c = i.Categorie # c is set to nil due to noMethodError
c.Item
NoMethodError: undefined method `Item' for nil:NilClass
Hopefully you understand a bit more what's going on now. If you want your code to work you should be doing this. Look closely, there are a few nuances:
i = Item.first # i is set to the first instance of Item
c = i.categorie # c is set to the instance of Categorie that i belongs to
is = c.items # returns an array consisting of all the Item instances that belong to the Categorie instance in c
We could also do this:
is.first # returns i
So where do all these handy methods come from? The categorie method inside i (i.category), the items method inside c (c.items)? The answer is they're created dynamically by Rails based on your inheritance and pulled into the relevant model by < ActiveRecord::Base.
By "based on your inheritance" I mean, how you've used the inheritance methods, belongs_to and has_many:
class Item < ActiveRecord::Base
belongs_to :categorie, :foreign_key => "catid" # creates categorie method that returns the instance of Categorie this instance of Item belongs to
end
class Categorie < ActiveRecord ...
has_many :item # creates items method that returns an array of all the instances of Item that belong to this instance of Categorie
end
I would also point out that Categorie is a pretty terrible Model name, purely because it's spelt wrongly. Maybe Type would be better?
You can do
Item.create (:catid => #categorie.id)
#categorie = Categorie.find(params[:id]) or with Categorie.all
place the each loop & find the id .
First you should have used Category for model because rails intelligently understands the plural categories or tables.
Secondly, you should have something like this;
class Item < ActiveRecord::Base
belongs_to :Category, :foreign_key => "catid"
end
class Categorie < ActiveRecord ...
has_many :items
end
i = Item.first
c = i.Category
c.items #to find all items that belong to the category c
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.
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.