Search code examples
ruby-on-railsactiverecordassociationsinstance

How to set an instance of a model as an attribute of another model in Rails?


I am fairly new to Rails and working on an app that will allow a user to make a List containing their top 5 Items of a certain category. The main issue I'm having is how to keep track of the List order (which should be allowed to change and will be different for each User)?

My Items can belong to many Lists and my Lists should have many Items so, as of now, I am using a has_and_belongs_to_many association for both my Item and List models.

My idea to keep track of the list order right now is to have my @list have 5 attributes: one for each ranking on the list (ie. :first, :second, :third, :fourth, :fifth) and I am attempting to associate the @item instance to the @list attribute (ie. @list.first = @item1, @list.second = @item2 , etc...). Right now I am saving the @list attribute to the @item ID (@list.first = 1), but I would prefer to be able to call the method .first or .second etc and have that point directly at the specific Item instance.

Here is my current schema for lists, items, and the join table list_nominations required for the has_and_belongs_to_many association-which I'm pretty sure I am not utilizing correctly (the :points attribute in items will be a way of keeping track of popularity of an item:

create_table "lists", force: :cascade do |t|
    t.integer "user_id"
    t.integer "category_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "first"
    t.string "second"
    t.string "third"
    t.string "fourth"
    t.string "fifth"
  end

create_table "items", force: :cascade do |t|
    t.string "name"
    t.integer "category_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "points", default: 0
  end

and here is the code currently in my List and Item models:

class List < ApplicationRecord
    belongs_to :user 
    belongs_to :category
    has_and_belongs_to_many :items
end

class Item < ApplicationRecord
    belongs_to :category
    has_and_belongs_to_many :lists
end

Is there a way to do this or any suggestions on a better way to keep track of the List order without creating multiple instances of the same Item?


Solution

  • I'm afraid your tables don't fit any known approach, you can achieve what you want but this is not a perfect nor a recommended solution, you could specify the primary key on many has_one associations inside lists but in items it's not very possible to have all lists in one association but you can have an instance method which query lists and returns the matched ones

    the hacky solution:

    
    class List < ApplicationRecord
        belongs_to :user 
        belongs_to :category
    
        has_one :first_item, primary_key: :first, class_name: "Item"
        has_one :second_item, primary_key: :second, class_name: "Item"
        has_one :third_item, primary_key: :third, class_name: "Item"
        has_one :fourth_item, primary_key: :fourth, class_name: "Item"
        has_one :fifth_item, primary_key: :fifth, class_name: "Item"
    end
    
    class Item < ApplicationRecord
        belongs_to :category
    
        def lists
            List.where(
                "first = ? OR second = ? OR third = ? OR fourth = ? OR fifth = ?", self.id, self.id, self.id, self.id, self.id
            )
        end
    end
    

    you can read about how to create a many-to-many relationship via has_and_belongs_to_many associations here: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association (your tables will need a field to properly point to each other)

    What I recommend doing is following a many-to-many through relationship guide (mono-transitive association) :

    you will need 1 extra table because you want to track the order(first,second, etc)

    DB:

    create_table "lists", force: :cascade do |t|
        .. all your other fields without first,second, etc..
    end
    
    create_table "items", force: :cascade do |t|
        .. all your other fields
    end
    
    create_table "lists_items", force: :cascade do |t|
        t.integer "list_id"
        t.integer "item_id"
        t.integer "rank" there is where you will store your order (first, second ..) bas as an integer
    end
    

    Models:

    class ListsItem < ApplicationRecord
        belongs_to :list
        belongs_to :item
    end
    
    class List < ApplicationRecord
        belongs_to :user 
        belongs_to :category
        has_many :lists_items, -> { order(:rank) }, limit: 5
        has_many :items, through: :lists_items
    end
    
    class Item < ApplicationRecord
        belongs_to :category
        has_many :lists_items
        has_many :lists, through: :lists_items
    end
    

    you can read more about many-to-many via has_many through here https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

    and the difference between the 2 approaches here https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many