Creating or updating a has_one ActiveRecord association - ruby-on-rails

I've been trying to get my head around ActiveRecord associations but I have hit a bit of a brick wall, and no matter how much I review the ActiveRecord documentation, I can't work out how to solve my problem.
I have two classes:
Property -> has_one :contract
Contract -> belongs_to :property
In my contract class, I have a method to create_or_update_from_xml
First I check to make sure the property in question exists.
property_unique_id = xml_node.css('property_id').text
property = Property.find_by_unique_id(property_unique_id)
next unless property
And this is where I get stuck, I have a hash of attributes for the contract, and what I want to do is something like:
if property.contract.nil?
# create a new one and populate it with attributes
else
# use the existing one and update it with attributes
I know how I would go about it if it was raw SQL, but I can't get my head around hte ActiveRecord approach.
Any hints past this road block would be hugely appreciated.
Thanks in advance.

if property.contract.nil?
property.create_contract(some_attributes)
else
property.contract.update_attributes(some_attributes)
end
Should do the trick. When you have a has_one or belongs_to association then you get build_foo and create_foo methods (which are like Foo.new and Foo.create). If the association already exists then property.contract is basically just a normal active record object.

Just another way of doing it using ruby OR-Equal trick
property.contract ||= property.build_contract
property.contract.update_attributes(some_attributes)
Update:
#KayWu is right, the above ||= trick will create the contract object in the first line, rather than just building it. An alternative would be
property.build_contract unless property.contract
property.contract.update_attributes(some_attributes)

Property.all.each do |f|
c = Contract.find_or_initialize_by(property_id: f.id)
c.update(some_attributes)
end
I don't know whether this is the best solution, but for me it more succinctly

From Rails 6.1 an onwards:
property.build_contract unless property.contract
property.contract.update(some_attributes)

Related

Ruby on Rails - has_one relation, how to check if it has an existing association?

I have a simple problem, related to associations.
I have a model for book, that has_one reservation.
Reservation belongs_to book.
I want to make sure in the create method of reservations controller that a book is not reserved already when a reservation is made. In other words, I need to check if any other reservation exists for that book. How do i do that?
EDIT:
Aaaand i made it, thanks everyone for the tips, learned some new stuff. When i tried offered solutions, I got no_method errors, or nil_class etc. That got me thinking, that the objects I'm trying to work on simply don't exist. Krule gave me the idea to use book.find, so i tried working with that.
Ultimately i got it working with:
book=Book.find_by_id(reservation_params[:book_id])
unless book.is_reserved?
Thanks everybody for your anwsers, I know it's basic stuff but i learned a lot. Cheers!
How about:
self.reservation.present?
This should return true if an association exists.
Little bit performance gain can be obtained if you use
#app/models/book.rb
def is_reserved?
Reservation.exists?(book_id: id)
end
#somewhere else
book = Book.find(id)
book.is_reserved?
#app/models/book.rb
def is_reserved?
!self.reservation.nil?
end
# Somewhere else
book = Book.find(id)
book.is_reserved?
Simply, use:
# book = Book.first
book.reservation.nil? # returns true if no reservation
# is associated with this book

Referring to instance in has_many (Rails)

I have a Game model which has_many :texts. The problem is that I have to order the texts differently depending on which game they belong to (yes, ugly, but it's legacy data). I created a Text.in_game_order_query(game) method, which returns the appropriate ordering.
My favourite solution would have been to place a default scope in the Text model, but that would require knowing which game they're part of. I also don't want to create separate classes for the texts for each game - there are many games, with more coming up, and all the newer ones will use the same ordering. So I had another idea: ordering texts in the has_many, when I do know which game they're part of:
has_many :texts, :order => Text.in_game_order_query(self)
However, self is the class here, so that doesn't work.
Is there really no other solution except calling #game.texts.in_game_order(#game) every single time??
I had a very similar problem recently and I was convinced that it wasn't possible in Rails but that I learned something very interesting.
You can declare a parameter for a scope and then not pass it in and it will pass in the parent object by default!
So, you can just do:
class Game < ActiveRecord
has_many :texts, -> (game) { Text.in_game_order_query(game) }
Believe or not, you don't have to pass in the game. Rails will do it magically for you. You can simply do:
game.texts
There is one caveat, though. This will not work presently in Rails if you have preloading enabled. If you do, you may get this warning:
DEPRECATION WARNING: The association scope 'texts' is instance dependent (the scope block takes an argument). Preloading happens before the individual instances are created. This means that there is no instance being passed to the association scope. This will most likely result in broken or incorrect behavior. Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
Following up using PradeepKumar's idea, I found the following solution to work
Assuming a class Block which has an attribute block_type, and a container class (say Page), you could have something like this:
class Page
...
has_many :blocks do
def ordered_by_type
# self is the array of blocks
self.sort_by(&:block_type)
end
end
...
end
Then when you call
page.blocks.ordered_by_type
you get what you want - defined by a Proc.
Obviously, the Proc could be much more complex and is not working in the SQL call but after there result set has been compiled.
UPDATE:
I re-read this post and my answer after a bunch of time, and I wonder if you could do something as simple as another method which you basically suggested yourself in the post.
What if you added a method to Game called ordered_texts
def ordered_texts
texts.in_game_order(self)
end
Does that solve the issue? Or does this method need to be chainable with other Game relation methods?
Would an Association extension be a possibility?
It seems that you could make this work:
module Legacy
def legacy_game_order
order(proxy_association.owner.custom_texts_order)
end
end
class Game << ActiveRecord::Base
includes Legacy
has_many :texts, :extend => Legacy
def custom_texts_order
# your custom query logic goes here
end
end
That way, given a game instance, you should be able to access instance's custom query without having to pass in self:
g = Game.find(123)
g.texts.legacy_game_order
Here is a way where you can do it,
has_many :texts, :order => lambda { Text.in_game_order_query(self) }
This is another way which I usually wont recommend(but will work),
has_many :texts do
def game_order(game)
find(:all, :order => Text.in_game_order_query(game))
end
end
and you can call them by,
game.texts.game_order(game)
Im not sure what your order/query looks like in the in_game_order_query class method but i believe you can do this
has_many :texts, :finder_sql => proc{Text.in_game_order_query(self)}
Just letting you know that I have never used this before but I would appreciate it if you let me know if this works for you or not.
Check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many for more documentation on :finder_sql
I think if you want runtime information processed you should get this done with:
has_many :texts, :order => proc{ {Text.in_game_order_query(self)} }

How to implement a last_modified_by (person) attribute on two unrelated models - Rails

I have a Record model and in order to edit this model, you must be logged in as an instance of Admin. I would like to have a column called last_modified_by which points to the Admin who last modified the Record. In the database, I was thinking it would be good in the records table to add a column that holds the Admin's id; however, the only way I know how to do that is with an association. These two models are not associated with each other so an association doesn't make a lot of sense. Is there any other way I might be able to accomplish this task without resorting to associations? Any advice would be much appreciated!
Hmm, I think the association is a good tool here. You might want to try to hack it somehow but I think nothing you can conjure up will ever be as good as an association via a foreign_key(also so fast). But perhaps you would like to name your association and do something like:
class Record < ActiveRecord::Base
belongs_to :culprit, :class_name => 'Admin', :foreign_key => 'last_modified_by'
end
or give it some more senseful naming?
You could create an Active Record before_save callback. The callback would save the admin's id into the last_modified_column. This would make sure the admin id is saved/updated each time there is a change to the model.
For example, assuming admin is #admin:
class Record < ActiveRecord::Base
before_save :save_last_modified
def save_last_modified
self.last_modified_column = #admin.id
end
As for getting #admin, you could employ a method similar to this, and set #admin = Admin.current (like User.current in the link) somewhere in the Record model.

Assigning rails association using strings

Say I have two models and one belongs to another. Now normaly you would assign an object to the association when populating the fields. Does rails allow overriding the set method so that the association assignment can be customised?
E.g
class Person
# something about shirts
end
class Shirt
belongs_to :person
def person=(p)
self.person = Person.find_or_create_by_name(p)
end
end
And then use something like so auto bind the association but using a string to do the searching and binding automatically. Is this possible?
s = Shirt.new
s.person = "Test Person"
Thanks
ROR Guides cover the association extension you need.
UPDATE:
Actually, overriding setter is not that bad, once you understand what you're doing. But you have to be careful, since it can cause infinite loop (as in your example). So if you're using Rails 3.2, you have to use super, in other case you have to use alias_method_chain.

How to intercept accepts_nested_attributes_for?

I have a Rails application, with two models: SalesTransactions and PurchaseOrders.
In the PurchaseOrders model, new entries are registered using 'purchase_order_number' as the key field. I use the create method of the model to search if that 'purchase_order_number' has been previously registered, and if so, reuse that record and use its id in the SalesTransaction record. If that name wasn't already registered, I go ahead and perform the create, and then use the new PurchaseOrder record id in the SalesTransaction (the foreign_id linking to the associated PO).
Note that I don't have the existing PurchaseOrder record id until I've done a look-up in the create method (so this is not a question of 'how do I update a record using 'accepts_nested_attributes_for'?', I can do that once I have the id).
In some situations, my application records a new SalesTransaction, and creates a new PurchaseOrder at the same time. It uses accepts_nested_attributes_for to create the PurchaseOrder record.
The problem appears to be that when using 'accepts_nested_attributes_for', create is not called and so my model does not have the opportunity to intercept the create, and look-up if the 'purchase_order_number' has already been registered and handle that case.
I'd appreciate suggestions as to how to intercept 'accepts_nested_attributes_for' creations to allow some pre-processing (i.e. look up if the PurchaseOrder record with that number already exists, and if so, use it).
Not all Sales have a PurchaseOrder, so the PurchaseOrder record is optional within a SalesTransaction.
(I've seen a kludge involving :reject_if, but that does not allow me to add the existing record id as the foreign_id within the parent record.)
Thanks.
You could use validate and save callbacks to do what you need.
Assuming the setup:
class SalesTransaction < ActiveRecord::Base
belongs_to :purchase_order, :foreign_key => "po_purchase_order_no",
:primary_key => "purchase_order_no"
accepts_nested_attributes_for :purchase_order
end
class PurchaseOrder < ActiveRecord::Base
has_many :sales_transactions, :foreign_key => "po_purchase_order_no",
:primary_key => "purchase_order_no"
before_validation :check_for_exisitng_po # maybe only on create?
accepts_nested_attributes_for :sales_transactions
private
def check_for_exisitng_po
existing_po = PurchaseOrder.find_by_purchase_order_no(self.purchase_order_no)
if existing_po
self.id = existing_po.id
self.reload # don't like this, also will overwrite incoming attrs
#new_record = false # tell AR this is not a new record
end
true
end
end
This should give back full use of accepts_nested_attributes_for again.
gist w/tests
Two ideas: Have you taken a look at association callbacks? Perhaps you can "intercept" accepts_nested_attributes_for at this level, using :before_add to check if it is already in the DB before creating a new record.
The other idea is to post-process instead. In an after_save/update you could look up all of the records with the name (that ought to be unique), and if there's more than one then merge them.
I was going to write a before_save function, but you say this:
It uses accepts_nested_attributes_for to create the PurchaseOrder record.
So in the SalesTransaction process flow, why look it up at all? You should just get the next one available... there shouldn't be a reason to search for something that didn't exist until NOW.
OK, I've left this question out there for a while, and offered a bounty, but I've not got the answer I'm looking for (though I certainly appreciate folk trying to help).
I'm concluding that I wasn't missing some trick and, at the time of writing, there isn't a neat solution, only work-arounds.
As such, I'm going to rewrite my App to avoid using accept_nested_attributes_for, and post the SalesTransaction and the PurchaseOrder records separately, so the create code can be applied in both cases.
A shame, as accept_nested... is pretty cool otherwise, but it's not complete enough in this case.
I still love Rails ;-)

Resources