DEV Community

Michael Lee πŸ•
Michael Lee πŸ•

Posted on

How would you setup a model in Rails to reference the same model type?

Let's say I have a model for a thing called "Chapter". I want to be able to reference the next chapter and the previous chapter for each chapter. How would you go about setting this up?

So what I'm looking for is something like this:

c = Chapter.first
c.next_chapter
Enter fullscreen mode Exit fullscreen mode

c.next_chapter should return a chapter object that is the next chapter.

Top comments (13)

Collapse
 
briankephart profile image
Brian Kephart

guides.rubyonrails.org/association...

# in migration
def change
  add_reference :chapters, :previous_chapter, foreign_key: { to_table: :chapters }
end

# chapter.rb
belongs_to :previous_chapter, class_name: β€˜Chapter’
has_one :next_chapter, class_name: β€˜Chapter’, foreign_key: :previous_chapter_id
Enter fullscreen mode Exit fullscreen mode
Collapse
 
michael profile image
Michael Lee πŸ•

Thanks Brian for the reply!

has_one :next_chapter, class_name: β€˜Chapter’, foreign_key: :previous_chapter_id

Why is the foreign_key the previous_chapter_id and not something like next_chapter_id?

Collapse
 
briankephart profile image
Brian Kephart

Because that’s a reference to the database column in your migration. That line basically says, β€œWhen the :next_chapter method is called, return the chapter that lists this one as its :previous_chapter.”

You could set up the migration to store the next chapter instead of the previous one. It’s the same process. I just thought it makes more sense this way because when you create a chapter, the previous one probably exists already, whereas the next one might not.

Thread Thread
 
michael profile image
Michael Lee πŸ•

Ahhh clever, thanks Brian for the help. I implemented this method and it worked as you described it :)

Collapse
 
mickeytgl profile image
Miguel • Edited

in your app/models/chapter.rb

def next_chapter
  return self if self == Chapter.last 

  Chapter.find self.id +1
end

Explanation:

The first line checks if the chapter that you called next_chapter on is the last one and if so, it just returns itself (You can change this to an error message or whatever you want). And if that's not the case, then we call find on chapter, which takes the Chapter ID as an argument and we just add one to the current ID. And voilΓ : Now you can call next_chapter on your Chapter instances

Collapse
 
michael profile image
Michael Lee πŸ•

Thanks Miguel for the tip on setting up the method :) this is super helpful.

Collapse
 
mickeytgl profile image
Miguel

Happy to help :)

Collapse
 
christopherlai profile image
Christopher Lai

Is there a parent object like book?

Collapse
 
michael profile image
Michael Lee πŸ•

Let's assume no.

Collapse
 
christopherlai profile image
Christopher Lai

I would go with Brian’s solution then. The other solutions work, but I would prefer to have a strong reference, in this case a foreign key stored in the Database.

Thread Thread
 
michael profile image
Michael Lee πŸ•

Yup that's the solution that I ended up going with. Thanks Christopher for the help!

Collapse
 
juanmanuelramallo profile image
Juan Manuel Ramallo

Assuming a Book class exist and it has many chapters, and also assuming that the chapter has a position number stored in the database, I'd do something like:

class Chapter < ApplicationRecord
  belongs_to :book

  def next_chapter
    book.chapters.find_by(position: position + 1)
  end

  def previous_chapter
    book.chapters.find_by(position: position - 1)
  end
end
Collapse
 
spenser profile image
Spenser • Edited

I'd question whether a method like this should sit in the model (should it actually have knowledge of its sibling persisted objects? Usually not)

Depending on your context, making a small class/service/whatever which determines position might be more suitable (and easier to test/maintain).