I have a rails API that injects primary key IDs from the client, as opposed to autogenerating through Rails. Specifically:
class ParentModel < ApplicationRecord
accepts_nested_attributes_for: child_models
has_many :child_models
end
class ChildModel < ApplicationRecord
belongs_to :parent_model
end
Nested data is created via:
#parent_object = ParentModel.find_or_initialize_by(id: parent_model_params[:id])
#parent_object.assign_attributes(parent_model_params)
#parent_object.save
If the ID for a child_object already exists in the database, the operation updates the child object as expected. However, if the child object is new, I get:
Couldnt find ChildModel with ID=b35e8f02... for ParentModel with ID=0c9b60f3...
In short: I'm trying to ensure rails creates child records (with the given IDs) when they don't exist, and continues to update existing child records if they do. Any thoughts on how to do that through nested attributes?
For those wondering, I couldn't find an elegant solution. Instead, I manually created a new child object for each new child before calling .assign_attributes. Eg:
parent_model_params[:child_model_attributes].each do |child_params|
next if #parent_object.child_ids.include?(child_params[:id])
#parent_object.child_objects << ChildModel.new(child_params)
end
#parent_object.assign_attributes(parent_model_params) # This no longer raises a RecordNotFound error
Related
I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.
Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.
So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.
I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
organisation.list_attributes.each do |a, t|
attribute a, t
end
end
Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:
{
name: String,
age: Integer
}
As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.
I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.
Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.
So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?
1 To clarify, if I were to use the hash directly like so:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
{
name: String,
age: Integer
}.each do |a, t|
attribute a, t
end
end
it would work, my issue is solely with getting a record's relation at this earlier point in time.
2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.
at the moment of instantiating (initializing or loading) a record, is
there a way I can retrieve a hash stored in a database column of one
of its relations?
Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:
class ListItem < ApplicationRecord
belongs_to :list
end
class List < ApplicationRecord
belongs_to :organisation
has_many :list_items
end
class Organisation < ApplicationRecord
has_many :lists
end
We can instantiate a ListItem and then retrieve data from anyone of its parents.
#list_item = ListItem.find(5) # assume that the proper inherited
foreign_keys exist for this and
its parent
#list = #list_item.list
#hash = #list.organisation.special_hash_of_org
And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:
class ListItem < ApplicationRecord
belongs_to :list
# this is called on ListItem.new and whenever we pull from our DB
after_initialize do |list_item|
puts "You have initialized a ListItem!"
list = list_item.list
hash = list.organisation.special_hash_of_org
end
end
But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!
Assume we have a parent model
class Parent < ActiveRecord::Base
has_one :child
end
We have already created a child object (persisted in database).
We then create a potential parent = Parent.new object for the child. Potential means that this parent may not ever be saved to the database and nothing should be changed in this case. Is it possible to set the child for the parent in a such a way that the child is updated (i.e. its foreign key is set) only after we call parent.save?
Of course this can be accomplished with hooks but I just wonder if there was an Rails inbuilt way to do this.
What does not work
parent.child = child will automatically save the parent so that's not an option, because we may not want to save the parent at all.
parent.build_child(child.attributes) will not work, because the child is already persisted
#app/models/child.rb
class Child < ActiveRecord::Base
belongs_to :parent
end
#app/controllers/parents_controller.rb #-> this can be any controller
class ParentsController < ApplicationController
def create
#child = Child.find params[:id]
#parent = #child.parent.new parent_params
#parent.save
end
private
def parent_params
params.require(:parent).permit(:x, :y, :z)
end
end
You have to remember that ActiveRecord is an "ORM", which means that it deals with your objects in memory, irrespective of how the database will store them.
You will not be able to populate a "foreign key" which isn't saved. In your case, child will not have its parent_id foreign key populated until parent is saved; however, ActiveRecord will basically pre-populate the various attributes depending on how you invoke the objects.
The above builds a new parent off an existing child.
Because you're defining the foreign_key in the child model, it will only be populated on save.
I'm creating a single associated record in a has_many association via nested_attributes like so
#Parent Model
has_many :child_models
#Parent Controller
def create
parentModel.update_attributes(parent_model_params_with_nested_child_attributes)
end
Pretty basic, but after the update created a new ChildModel I want to add some other data to it, so I need to get a reference to the ChildModel that was just created.
parentModel.childModels.order("id DESC").first
since that SHOULD contain the most recently created association, but I'm wondering if A) that's true and B) there isn't a better way.
New to Rails and Ruby and trying to do things correctly.
Here are my models. Everything works fine, but I want to do things the "right" way so to speak.
I have an import process that takes a CSV and tries to either create a new record or update an existing one.
So the process is 1.) parse csv row 2.) find or create record 3.) save record
I have this working perfectly, but the code seems like it could be improved. If ParcelType wasn't involved it would be fine, since I'm creating/retrieving a parcel FROM the Manufacturer, that foreign key is pre-populated for me. But the ParcelType isn't. Anyway to have both Type and Manufacturer pre-populated since I'm using them both in the search?
CSV row can have multiple manufacturers per row (results in 2 almost identical rows, just with diff mfr_id) so that's what the .each is about
manufacturer_id.split(";").each do |mfr_string|
mfr = Manufacturer.find_by_name(mfr_string)
# If it's a mfr we don't care about, don't put it in the db
next if mfr.nil?
# Unique parcel is defined by it's manufacturer, it's type, it's model number, and it's reference_number
parcel = mfr.parcels.of_type('FR').find_or_initialize_by_model_number_and_reference_number(attributes[:model_number], attributes[:reference_number])
parcel.assign_attributes(attributes)
# this line in particular is a bummer. if it finds a parcel and I'm updating, this line is superfulous, only necessary when it's a new parcel
parcel.parcel_type = ParcelType.find_by_code('FR')
parcel.save!
end
class Parcel < ActiveRecord::Base
belongs_to :parcel_type
belongs_to :manufacturer
def self.of_type(type)
joins(:parcel_type).where(:parcel_types => {:code => type.upcase}).readonly(false) unless type.nil?
end
end
class Manufacturer < ActiveRecord::Base
has_many :parcels
end
class ParcelType < ActiveRecord::Base
has_many :parcels
end
It sounds like the new_record? method is what you're looking for.
new_record?() public
Returns true if this object hasn’t been saved yet — that is, a record
for the object doesn’t exist yet; otherwise, returns false.
The following will only execute if the parcel object is indeed a new record:
parcel.parcel_type = ParcelType.find_by_code('FR') if parcel.new_record?
What about 'find_or_create'?
I have wanted to use this from a long time, check these links.
Usage:
http://rubyquicktips.com/post/344181578/find-or-create-an-object-in-one-command
Several attributes:
Rails find_or_create by more than one attribute?
Extra:
How can I pass multiple attributes to find_or_create_by in Rails 3?
Using Rails 2.3.14, I'm looking for way to access the owner of an ActiveRecord object after it was build (but before save) to get some values from the owner. Seems to be simple, but my approach always fires an unnecessary database query.
Example:
class Parent < ActiveRecord::Base
has_many :children
end
class Child < ActiveRecord::Base
belongs_to :parent
def after_initialize
self.some_value = parent.some_value
# This fires an additional database query to get the parent
end
end
parent = Parent.find(1)
# SELECT * FROM `parents` WHERE (`parents`.`id` = 1)
child = parent.children.build
# Same SELECT query is fired again, but of course not needed
I'm looking for a way to access the association object (here: parent) without doing an additional database access. How can this be done?
In Rails 3, there's a new option, :inverse_of, for belongs_to/has_many to do this, but not in Rails 2. Maybe you have to implement similar function by yourself.