0

I'm new to Ruby on Rails, and I'm developing a backend API.

Currently, I got 2 Active Record Models called Book and User.

Active Record Model

class Book < ActiveRecord::Base
    has_and_belongs_to_many :users
end

class User < ActiveRecord::Base
    has_and_belongs_to_many :books
end

DB Schema Model

create_table :books do |t|
  t.string "title"
end

create_table :users do |t|
  t.string "name"
end

#User favourite books
create_join_table :users, :books do |t|
  t.index [:user_id, :book_id]
  t.index [:book_id, :user_id]
end

#User read books
create_join_table :users, :books do |t|
  t.index [:user_id, :book_id]
  t.index [:book_id, :user_id]
  t.integer "read_pages"
  t.string "status"
  t.integer "rating"
  t.datetime "start_date"
  t.datetime "finish_date"
end

QUESTION

I'd like to create 2 join tables, one for those books added to the user favourites list, another one for those books that a user has read.

Both tables share user_id & book_id, howerver, the second one has more data since it is a record.

  1. Active Record naming convention creates a table named as users_books automatically. So when I migrate this, it reports to me the following error:

Index name 'index_books_users_on_user_id_and_book_id' on table 'books_users' already exists

  1. How do I rename the second join table name?
Jiahui Chen
  • 75
  • 3
  • 10

2 Answers2

5

How do I rename the second join table name?

Pass the table_name: option:

create_join_table :users, :books, table_name: :favorite_books do |t|
  t.index [:user_id, :book_id]
end

You also need to use a unique names for the associations and tell rails whats going on since it can be derived from the name:

class User < ApplicationRecord
  has_and_belongs_to_many :books
  has_and_belongs_to_many :favorite_books, 
    join_table: 'favorite_books',
    class_name: 'Book',
    inverse_of: :favorite_books
end

class Book
  has_and_belongs_to_many :users
  # for lack of a better name?
  has_and_belongs_to_many :favorite_users, 
    join_table: 'favorite_books',
    class_name: 'User',
    inverse_of: :favorite_books
end

The unique names are necessary since you would just be clobbering the previous associations if you used the same names.

But...

has_and_belongs_to_many and create_join_table are pretty useless as they won't let you access any of the additional columns on the table and don't provide such niceties like primary keys or timestamps. The documentation says:

Use has_and_belongs_to_many when working with legacy schemas or when you never work directly with the relationship itself.

But how are you ever supposed to know if you're going to want those features down the line? And you're stuck there with a table with just two foreign keys. Its a much better idea to go with has_many through: and switch to has_and_belongs_to_many if the memory usage ever becomes a problem (it ain't gonna happen).

TLDR; has_and_belongs_to_many sucks. Use has_many through: instead.

max
  • 96,212
  • 14
  • 104
  • 165
  • Thanks @max for the meticulous explanation. After a redesign of my database schema, I decided to not to create a second join table, I just added a new boolean field to the already created join table. However, I found your answer really helpeful, so if I ever encounter the same problem I'll integrate your solution. Oh yeah, and thanks for the last piece of helpeful advice! – Jiahui Chen Jul 24 '20 at 18:09
  • By the way, yesterday I posted another question: https://stackoverflow.com/questions/63047441/store-join-model-data-in-rails Sorry for bothering you too much, but I'm really new to backend development with Rails. – Jiahui Chen Jul 24 '20 at 18:12
1

You should use has_many :through association if you want to save other than primary keys for many to many relationship. For more details, refer Rails guides.Your models should be like:

class Book < ActiveRecord::Base
    has_many :read_books
    has_many :users, through: :read_books
end

class User < ActiveRecord::Base
    has_many :read_books
    has_many :books, through: :read_books
end

class ReadBook < ActiveRecord::Base
    belongs_to :user
    belongs_to :book
end

And you can also make one field/flag(is_favourite) in read_books and create a scope in read_books for favourites like

scope :favourites, -> { where(is_favourite: true) }
Chakreshwar Sharma
  • 2,571
  • 1
  • 11
  • 35
  • It seems pretty logical to have a boolean type field in read_books table. However, what if a user wants to add a book to her/his favourite books list even if it's not read (I know, it doesn't make any sense, but it could happen). So, do you know how to create 2 join tables (and changing the name of the second one)? Thanks – Jiahui Chen Jul 24 '20 at 13:28
  • got it, but you should `has_many through` association for read books table – Chakreshwar Sharma Jul 24 '20 at 13:36
  • @Viktor Yeah, after some design pattern checking I finally decided to add those fields, thank you very much – Jiahui Chen Jul 24 '20 at 17:09