jackbot
jackbot

Reputation: 3021

Rails 2.3 dynamic has_many conditions based on other association

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.

Upvotes: 2

Views: 562

Answers (3)

Yanhao
Yanhao

Reputation: 5294

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.

Upvotes: 1

klochner
klochner

Reputation: 8125

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.

Upvotes: 3

cdesrosiers
cdesrosiers

Reputation: 8892

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.

Upvotes: 1

Related Questions