Does pushing an entity to a it's parent collection causes the collection to be loaded?
e.g.:
Parent.childs << a_child
Is the parent.childs "array" now loaded with ALL the childs or just "a_child" ? i.e. will a SQL "select * from childs where parent_id = ?" statement will be executed before adding to the collection?
EDIT:
http://apidock.com/rails/v3.2.3/ActiveRecord/Associations/CollectionAssociation/concat_records seems to call "add_target" BEFORE calling "insert_record" which adds the entity to the #target array...
No (thank god)! Parent.childs is a Proxy and '<<' will just trigger the creation/update of a_child
More info on How do rails association methods work?
Also in the rails comments of associations/collection_associations.rb
You need to be careful with assumptions regarding the target: The
proxy does not fetch records from the database until it needs them,
but new ones created with +build+ are added to the target. So, the
target may be non-empty and still lack children waiting to be read
from the database. If you look directly to the database you cannot
assume that's the entire collection because new records may have been
added to the target, etc.
A quick check on my console confirmed there the children aren't loaded.
parent.children << child; nil
=> only parent and child where loaded....
Related
I need all the documents in one of my collections to create association in between my parent model to child. The problem is I only have the string of my ObjectId. So I am finding the object by string and then set via parent.child = foundObject. So, to achieve this I created a private method as below, to not to create DB request each time I need that child object.
def childs
#childs ||= Child.all
end
But this is not working as expected. When I run ModelName.all it returns below result; not all the docs in collection.
=>
#<Mongoid::Criteria
selector: {}
options: {}
class: ModelName
embedded: false>
And this causes my loop to create another DB request each time I try to associate child to parent. I prevent this by using below method.
def childs
#childs ||= Child.all.select { |v| v.id.present? }
end
I believe there should be a way of collecting all documents in MongoDB, I know the idea of Mongoid::Criteria and what it actually does. But in some case, I need all the objects to be stored in one variable. Do not want to create unwanted DB queries each time I need one specific document in a model.
I could not find a way to solve this specific problem and I think it's kind of impossible since MongoDB is not a relational DB It's quite hard to collect information at the same time with querying. What I used is "MongoDB views" and this solved a lot. Here is the docs. There you can read and find yourself an approach to figure out your own problem.
So I am building an associated object through a main object like so:
item.associated_items.build({name: "Machine", color: "Grey"})
and then in another method calling item.save. However I am getting an ActiveRecord::ReadOnlyRecord error. I read in the docs that
Records loaded through joins with piggy-back attributes will be marked as read only since they cannot be saved.
so I think that is what is happening here. But
I dont't know why that is happening. I have called save on an object with a new associated record before and had no problems.
What do the docs mean when they say "piggy-back attributes"?
Is there a way to make the save happen by doing something like item.associated_items.readonly(false).build(attributes). I tried that and it didnt work, but I'm hoping there is another way.
edit: I just tried
new_associated_item = AssociatedItem.new({attributes})
item.associated_items << new_associated_item
and the later method calls
item.save
and the read only exception still happens.
edit2: MurifoX asked me about how Item is being loaded. The above code is happening in a couple of service objects. The process is
Controller
owner = Owner.includes(:medallions).references(:medallions).find_by_id(params[:id])
later
creator = NoticeCreator.new(owner)
creator.run
NoticeCreator
def initialize #effectively
medallion_notice_creators = []
owner.medallions.some_medallion_scope.each do |medallion|
medallion_notice_creator = MedallionNoticeCreator.new(medallion)
medallion_notice_creator.prepare
medallion_notice_creators << medallion_notice_creator
end
end
later after looping through the medallion notice creators
def prepare
medallion.notices.build(attributes)
end
later
medallion_notice_creators.each do |medallion_notice_creator|
medallion_notice_creator.medallion.save
end
Apologies if the code seems convoluted. There is a bunch of stuff going on and I'm trying to condense the code and anonymize it.
Objects created with joins or includes, which is your case, are marked read-only because you are making a giant query with joins and stuff and preloading nested objects within your main one. (ActiveRecord can become confused with so many attributes and don't know how to build the main object, so it marks readonly on it.)
As you have noticed, this won't happen if you create you object with a simple find, as the only attributes received from the query are from the object itself.
I don't know why you are eager loading all of this associations, maybe it is from some rule in your code, but you should try to create a simple owner object using Owner.find(params[:id]), and lazy loading the associations when needed, so this way you can build nested associations on the simple object and save them.
Suppose you have two ActiveRecord models - Problem and ProblemSet.
You have a #problem_set object, and you want to check if it has a problem with a certain title attribute.
You could say:
#problem_set.problems.where(:title => "Adding Numbers").any?
which returns true, by running the optimal SQL:
SELECT COUNT(*) FROM "problems" INNER JOIN "problem_sets_problems" ON "problems"."id" = "problem_sets_problems"."problem_id" WHERE "problem_sets_problems"."problem_set_id" = 1 AND "problems"."title" = 'Adding Numbers'
However, if #problem_set was in memory, ie, you got #problem_set by:
#problem_set = ProblemSet.new()
#problem_set.problems << Problem.new(:title = "Adding Numbers")
Then you will not be able to find the problem (ie. it returns false!). This is because the following SQL is run:
SELECT COUNT(*) FROM "problems" INNER JOIN "problem_sets_problems" ON "problems"."id" = "problem_sets_problems"."problem_id" WHERE "problem_sets_problems"."problem_set_id" IS NULL AND "problems"."title" = 'Adding Numbers'
A possible way to perform the check correctly for both persistent objects and in-memory objects, is:
#problem_set.problems.map(&:title).include? "Adding Numbers"
Which correctly returns true in both cases. However, in the case of a persistent object, it runs the non-optimal SQL (which retrieves all problems):
SELECT "problems".* FROM "problems" INNER JOIN "problem_sets_problems" ON "problems"."id" = "problem_sets_problems"."problem_id" WHERE "problem_sets_problems"."problem_set_id" = 1
Question: Is there a way to use the same code to check for both persistent objects and in-memory objects, while running optimal SQL code?
Note that a solution which checks for object persistence is permitted (but I don't see how to check the dirtiness of the collection). However, it should still work if a persistent object is modified (ie. the association collection attribute becomes dirty, and therefore the result from an SQL query would be out-of-date).
Ok, I finally worked it out.
Browsing through obscure rails functions, I found the persisted? method. You can use #problem_set.persisted? to check if the object is persistent or in-memory only.
So the answer is:
if #problem_set.persisted?
#problem_set.problems.where(:title => "Adding Numbers").any?
else
#problem_set.problems.map(&:title).include? "Adding Numbers"
end
The remaining question is, what about persistent objects where the association collection is dirty? Well, by experimentation, I found out that it doesn't really happen. When you add an object to the collection, for example, one of the following:
#problem_set.problems << Problem.new(:title => "hello")
#problem_set.problems.push Problem.new(:title => "hello")
ActiveRecord immediately saves the data. Similarly, it immediately destroys the row from the associations table when you say:
#problem_set.problems.delete(#problem_set.problems[2])
That means, although there is no such method as #problem_set.problems_changed?, if there was, the current implementation would result in problems_changed? always returning false.
In effect, the collection<<, collection.push, collection.delete methods auto-save (ie. calls save automatically).
I have a Product object that has many InventoryCurrent objects. Once a month all inventory transactions will be summed and a new InventoryCurrent record will be created for each product in each warehouse. This is done so that the system does not have to do hundreds of calculations every time current inventory is requested.
I have created a from_warehouse scope as follows:
scope :from_warehouse, lambda {|warehouse| where(:warehouse_id =>warehouse) }
I would like to make a call to get the current inventory as follows:
product.current_inventories.from_warehouse(2).recent
This should return either the most current InventoryCurrent, or if one hasn't been created yet (because the product was just added) then it should return a new InventoryCurrent object with the proper warehouse_id and product_id set.
The problem is that scope returns a recordset, not a single object. I can't put this scope in a method and call .first because that might generate an error if the recordset is empty.
Any suggestions?
use find_or_create_by_warehouse_id_and_product_id, or find_or_initialize_by_warehouse_id_and_product_id. see this for more information.
I have a POCO entity Report with a collection of a related POCO entity Reference. When creating a Report I get an ICollection<int> of ids. I use this collection to query the reference repository to get an ICollection<Reference> like so:
from r in referencesRepository.References
where viewModel.ReferenceIds.Contains(r.Id)
select r
I would like to connect the collection straight to Report like so:
report.References = from r in referencesRepository.References
where viewModel.ReferenceIds.Contains(r.Id)
select r;
This doesn't work because References is an ICollection and the result is an IEnumerable. I can do ToList(), but I think I will then load all of the references into memory. There also is no AddRange() function.
I would like to be able to do this without loading them into memory.
My question is very similar to this one. There, the only solution was to loop through the items and add them one by one. Except in this question the list of references does not come from the database (which seemed to matter). In my case, the collection does come from the database. So I hope that it is somehow possible.
Thanks in advance.
When working with entity framework you must load objects into memory if you want to work with them so basically you can do something like this:
report.References = (from r in referencesRepository.References
where viewModel.ReferenceIds.Contains(r.Id)
select r).ToList();
Other approach is using dummy objects but it can cause other problems. Dummy object is new instance of Reference object which have only Id set to PK of existing object in DB - it will act like that existing object. The problem is that when you add Report object to context you must manually set each instance of Reference in ObjectStateManager to Unchanged state otherwise it will insert it to DB.
report.References = viewModel.ReferenceIds.Select(i => new Reference { Id = i }).ToList();
// later in Report repository
context.Reports.AddObject(report);
foreach (var reference in report.References)
{
context.ObjectStateManager.ChangeObjectState(reference, EntityState.Unchanged);
}