Creating a many-to-many record on Rails

梦想的初衷 提交于 2020-12-06 12:12:36

问题


I have a simple task list app that has users and lists on it, the users are managed by Devise, and can create task lists, as well as favorite lists created by other users, or by themself. The relation of ownership between users and lists were easy to establish, but I am having trouble setting up the relation of a user favoriting a list. I envision it being a many-to-many relation after all, a user can favorite many lists and a list can be favorited by many users, this relationship happening on top of another already existing one-to-many relationship of list ownership by a user gave me some pause as to whether this is good practice to do, but I proceeded with my attempt regardless.

Currently I have two models, one for the user, and one for the list, and I tried to create a migration for the favorites by running rails g migration CreateJoinTableFavorites users lists, which resulted in the following migration

class CreateJoinTableFavorites < ActiveRecord::Migration[5.2]
  def change
    create_join_table :users, :lists do |t|
      t.index [:user_id, :list_id] <-- I uncommented this line
      # t.index [:list_id, :user_id]
      t.timestamps <-- I added this line
    end
  end
end

I thought this would create a table named "Favorites" that would automatically link users and lists, but instead it created a table called "lists_users". Now I am stuck as to what to do next. I have read that I need to create a model for this join table, but I don't know how to go about doing that. What command do I run? rails g model Favorites? rails g model ListsUsers? do I also inform the fields I want to add such as rails g model Favorites user_id:integer list_id:integer, or is there another better way to do it such as perhaps rails g model Favorites user:references list:references? What's the best practice here

Beyond that, I have added a button inside my list#show view for the user to click to add that list to their favorites, and had some trouble routing it. What I did was create a button like this:

<%= button_to 'Add to favorites', add_favorites_path({list_id: @list.id}), method: :post %>

as well as a new route:

post 'add_favorites', to: 'lists#add_favorites'

Though this I managed to have access to the list id and user id in that action, now I don't know how to proceed to create the "favorite" database entry in my lists_users table. To illustrate, I'll paste here my "add_favorite" action

def add_favorites
    user_id = current_user.id
    list_id = params[:list_id]

    #TODO: create the relation in lists_items table
end

I'm aware that I can't get this to work without the model for the join table, but even if I had that model, I haven't had much luck searching for what to do within the controller to create that relation. Anyway, my models are as follows:

class List < ApplicationRecord
    belongs_to :user
    has_many :users, through: :lists_users
end
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :lists
  has_many :lists, through: :lists_users
end

So to summarize, I am aware that I am missing a model for the join table, and would like a step-by-step as to how to create it, what name to give it, etc, as well as how to proceed within my action in my controller to create a new favorite entry


回答1:


There are two ways to create a many-to-many relation in Rails. What you're doing seems to conflate the two, which I suspect is the source of your problem.

Briefly, the two methods are:

1) has_many :other_models, through: :relation or

2) has_and_belongs_to_many :other_models

The main difference being that the "has_many through" method expects the join table to be a separate model which can be handled independently of this relationship if need be, while the "has_and_belongs_to_many" method does not require the join table to have a corresponding model. In the latter case, you will not be able to deal with the join table independently. (This makes timestamps on the join table useless, by the way.)

Which method you should go with depends on your use case. The docs summarize the criteria nicely:

The simplest rule of thumb is that you should set up a has_many :through relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a has_and_belongs_to_many relationship (though you'll need to remember to create the joining table in the database). (emphasis added)


Now for your question: When you use create_join_table, you're treating it as though you're setting things up for a has_and_belongs_to_many relation. create_join_table will create a table named "#{table1}_#{table2}" with ids pointing to those tables. It alphabetizes them too, which is why you got "lists_users" instead of "users_lists". This is in fact the standard naming convention for rails join tables if you are planning on using has_and_belongs_to_many, and generally shouldn't be renamed.

If you really want to use has_and_belongs_to_many, keep the migration with the create_join_table and just do the following in your models:

# user.rb
class User
  has_and_belongs_to_many :lists
end

# list.rb
class List
  has_and_belongs_to_many :users
end

And voila. No Favorite model is needed, and rails is smart enough to handle the relationships through the table on its own. Although a bit easier, the downside is, as stated above, that you won't be able to deal with the join table as an independent model. (Again, timestamps on the join table are useless in this case, as Rails won't set them.)

Edit: Since you can't directly touch lists_users, you'd create relationships by setting the lists relation on a user, or by setting the users relation on lists, like so:

def add_favorites
  list = List.find(params[:list_id])
  current_user.lists << list # creates the corresponding entry in lists_users

  # Don't forget to test how this works when the current_user has already favorited a list!
  # If you want to prevent that from happening, try
  # current_user.lists << list unless current_user.lists.include?(list)

  # Alternatively you can do the assignment in reverse:
  # list.users << current_user

  # Again, because the join table is not an independent model, Rails won't be able to do much to other columns on lists_users out of the box.
  # This includes timestamps
end

On the other hand, if you want to use "has_many through", don't use create_join_table. If you're using has_many through, the join table should be thought of almost as an entirely separate model, that just happens to have two foreign keys and tie two other models together in a many-to-many relationship. In this case, you'd do something like:

# migration
class CreateFavorites < ActiveRecord::Migration[5.2]
  def change
    create_table :favorites do |t|
      t.references :list
      t.references :user
      t.timestamps
    end
  end
end

# user.rb
class User
  has_many :favorites
  has_many :lists, through: :favorites
end

# list.rb
class List
  has_many :favorites
  has_many :users, through: :favorites
end

# favorite.rb
class Favorite
  belongs_to :list
  belongs_to :user
end

# controller
def add_favorites
  # You actually have a Favorite model in this case, while you don't in the other. The Favorite model can be more or less independent of the List and User, and can be given other attributes like timestamps.
  # It's the rails methods like `save`, `create`, and `update` that set timestamps, so this will track those for you as any other model.
  Favorite.create(list_id: params[:list_id], user: current_user)
end

You might want to reflect on which method to use. Again, this really depends on your use case, and on the criteria above. Personally, when I'm not sure, I prefer the "has_many through" method as it gives you more tools to work with and is generally more flexible.




回答2:


You may try following :

class User
  has_and_belongs_to_many :lists
end

class List
  has_and_belongs_to_many :users
end

class CreateUsersAndLists
  def change
    create_table :users do |t|
      # Code
    end

    create_table :lists do |t|
      # Code
    end

    create_table :users_lists id: false do |t|
      t.belongs_to :user, index: true
      t.belongs_to :list, index: true
      t.boolean :is_favourite
    end
  end
end


来源:https://stackoverflow.com/questions/57015988/creating-a-many-to-many-record-on-rails

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!