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

Michael Lee 🍕 @michael

About: Maker of things, giver of high-fives 🖐

Location:
Cary, NC
Joined:
Apr 14, 2017

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

Publish Date: Jun 11 '19
7 13

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.

Comments 13 total

  • Miguel
    MiguelJun 11, 2019

    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

    • Michael Lee 🍕
      Michael Lee 🍕Jun 11, 2019

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

      • Miguel
        MiguelJun 11, 2019

        Happy to help :)

  • Brian Kephart
    Brian KephartJun 11, 2019

    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
    • Michael Lee 🍕
      Michael Lee 🍕Jun 11, 2019

      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?

      • Brian Kephart
        Brian KephartJun 11, 2019

        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.

        • Michael Lee 🍕
          Michael Lee 🍕Jun 12, 2019

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

  • Juan Manuel Ramallo
    Juan Manuel RamalloJun 12, 2019

    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
    
  • Christopher Lai
    Christopher LaiJun 12, 2019

    Is there a parent object like book?

    • Michael Lee 🍕
      Michael Lee 🍕Jun 12, 2019

      Let's assume no.

      • Christopher Lai
        Christopher LaiJun 12, 2019

        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.

        • Michael Lee 🍕
          Michael Lee 🍕Jun 12, 2019

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

  • Spenser
    SpenserJun 13, 2019

    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).

Add comment