I have an ActiveRecord model called Books which has a has_one association on authors and a has_many association on publishers. So the following code is all good
books.publishers
Now I have another AR model, digital_publishers which is similar but which I would like to transparently use if the book's author responds to digital? - let me explain with some code
normal_book = Book.find(1)
normal_book.author.digital? #=> false
normal_book.publishers #=> [Publisher1, Publisher2, ...]
digital_book = Book.find(2)
digital_book.digital? #=> true
digital_book.publishers #=> I want to use the DigitalPublishers class here
So if the book's author is digital (the author is set through a has_one :author association so it's not as simple as having a has_many with a SQL condition on the books table), I still want to be able to call .publishers on it, but have that return a list of DigitalPublishers, so I want some condition on my has_many association that first checks if the book is digital, and if it is, use the DigitalPublishers class instead of the Publishers class.
I tried using an after_find callback using the following code:
after_find :alias_digital_publisher
def alias_digital_publisher
if self.author.digital?
def publishers
return self.digital_publishers
end
end
end
But this didn't seem to do the trick. Also, I'm using Rails 2.3.
I need more information about the project to really make a recommendation, but here are some thoughts to consider:
1. Publishers shouldn't belong to books
I'm guessing a publisher may be linked to more than one book, so it doesn't make a lot of sense that they belong_to books. I would consider
#Book.rb
has_many :publishers, :through=>:publications
2. Store digital publishers in the publishers table
Either use Single Table Inheritance (STI) for digital publishers with a type column of DigitalPublisher, or just add a boolean indicating whether a publisher is digital.
This way you can just call book.publishers, and you would get publishers that may or may not be digital, depending on which were assigned.
The trick is that you would need to ensure that only digital publishers are assigned to books with a digital author. This makes sense to me though.
3. (alternatively) Add a method for publishers
def book_publishers
author.digital? ? digital_publishers : publshers
end
I'm not really a fan of this option, I think you're better off having all the publishers in one table.
Have a look at this section from Rails Guides v-2.3.11. In particular note the following:
The after_initialize and after_find callbacks are a bit different from the others. They have no before_* counterparts, and the only way to register them is by defining them as regular methods. If you try to register after_initialize or after_find using macro-style class methods, they will just be ignored.
Basically, try defining your after_find as
def after_find
...
end
If that doesn't work, it might be because the book's fields haven't been initialized, so try after_initialize instead.
My solution is very simple.
class Book < ActiveRecord::Base
has_many :normal_publishers, :class_name => 'Publisher'
has_many :digital_publishers, :class_name => 'DigitalPublisher'
def publishers
if self.digital?
self.digital_publishers
else
self.normal_publishers
end
end
end
You can still chain some methods, like book.publishers.count, book.publishers.find(...).
If you need book.publisher_ids, book.publisher_ids=, book.publishers=, you can define these methods like book.publishers.
The above code works on Rails 2.3.12.
UPDATE: Sorry, I noticed klochner's alternate solution after I had posted this.
Related
I have 2 Rails models: Book and Category, where a book belongs_to a category, a category has_many books.
The category name is shown in each book's page, and pages are cached.
If I change a category name (say, from 'Sci Fi' to 'Science Fiction'), then all corresponding book pages will be stale, and books need to be "touched" in order to trigger HTML regeneration.
It would seem to make sense to be able to do:
class Category << ActiveRecord::Base
has_many :books, touch: true
end
But the option is unavailable, I guess because the touch mechanism would instantiate each object, which could result in a major performance hit for has_many relationships.
To avoid that, I am using raw SQL as follows:
class Category << ActiveRecord::Base
has_many :books
after_update -> {
ActiveRecord::Base.connection.execute "UPDATE books SET updated_at='#{current_time_string}' WHERE category_id=#{id})"
}
end
Which is pretty terrible.
Is there a better way?
You can't use touch on has_many association, it works only with belongs_to, that's a fact.
If I understand correctly what you want, the answers with touch:true in the Book model won't work, because the Book object will not be updated when You change the Category model and the view will not regenerating.
So I think your solution is the best for that. (You can use also books.update_all(updated_at: Time.now))
As of Rails 6, there is a touch_all method available on ActiveRecord::Relation that handles this sort of thing with one query. There is a pretty good blog article on it here.
It is only available on the belongs_to method which should be in your books model. So you can still use it.
I have two models:
class Customer < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :customer
validates :customer, presence: true
end
Then, in my controller, I would expect to be able to create both in
"one" sweep:
#customer = Customer.new
#customer.contacts.build
#customer.save
This, fails (unfortunately translations are on, It translates to
something like: Contact: customer cannot be blank.)
#customer.errors.messages #=> :contacts=>["translation missing: en.activerecord.errors.models.customer.attributes.contacts.invalid"]}
When inspecting the models, indeed, #customer.contacts.first.customer
is nil. Which, somehow, makes sense, since the #customer has not
been saved, and thus has no id.
How can I build such associated models, then save/create them, so that:
No models are persisted if one is invalid,
the errors can be read out in one list, rather then combining the
error-messages from all the models,
and keep my code concise?
From rails api doc
If you are going to modify the association (rather than just read from it), then it is a good idea to set the :inverse_of option on the source association on the join model. This allows associated records to be built which will automatically create the appropriate join model records when they are saved. (See the ‘Association Join Models’ section above.)
So simply add :inverse_of to relationship declaration (has_many, belongs_to etc) will make active_record save models in the right order.
The first thing that came to my mind - just get rid of that validation.
Second thing that came to mind - save the customer first and them build the contact.
Third thing: use :inverse_of when you declare the relationship. Might help as well.
You can save newly created related models in a single database transaction but not with a single call to save method. Some ORMs (e.g. LINQToSQL and Entity Framework) can do it but ActiveRecord can't. Just use ActiveRecord::Base.transaction method to make sure that either both models are saved or none of them. More about ActiveRecord and transactions here http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
Let's say I have two models
class Client < ActiveRecord::Base
has_many :accounts
end
class Account < ActiveRecord::Base
belongs_to :client
end
Now, I want to have convenient method to access approved accounts(:approved => true).
What is better to use, and why?
has_many: approved_accounts, -> { where :approved => true } for Client model
or
scope: approved, -> { where :approved => true } for Account model
Short answer, It Depends. For Long answer, kindly read on...
Conditional associations allows us to Customize the query that ActiveRecord will use to fetch the association. Use when you are sure that this condition is permanent & you will never-ever need access to data that doesn't fit the conditional(atleast not in this model) as Conditional associations are APPLIED to every query to ActiveRecord does to the association from that particular model.
Scope It is basically, a class method for retrieving and querying objects. so what you are actually doing is defining the following method in your model.
class Client < ActiveRecord::Base
self.approved
where(:approved => true)
end
end
so scope is generally use to define short names for one or more regularly used query customization. But important difference is that scope is not auto-applied unless you use default_scope but conditional associations are auto-applied.
In your case, you want to show unapproved accounts in this model? If not, then use conditional association. Else use scope
According to me, the scope solution seems the better:
As you probably know, to get the approved accounts of a client, you can do: approved_accounts = client.accounts.approved which is not less legible than: approved_accounts = client.approved_accounts
So not many difference here. But if in the future you want the list of all approved accounts (for statistics or whatever), with the scope solution, a single approved_accounts = Account.approved will be enough. But if you choose the client side, it will be trickier to get (understand: you will have to use the Client model).
Just consider than being approved is a property of an Account more than a property of a Client and it should be clearer that the scope is the best solution.
Hope this clarifies things.
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)} }
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 ;-)