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.
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
How do I rename the second join table name?
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.
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) }
Related
Every post has a userview, and each userview has many users. I want one single many to many to have a simple .add() and .remove() function like django. How do I place the current_user into the many-to-many relationship of the views?
I found this:
#post.userview.users << current_user
But it brings up some SQL error. It's suggesting I add a post_id:
ERROR: column userviews.post_id does not exist
LINE 1: SELECT "userviews".* FROM "userviews" WHERE "userviews"."po...
^
HINT: Perhaps you meant to reference the column "userviews.posts_id".
After the answer, now the error is:
can't write unknown attribute `userview_id`
Because I changed the migrations around a little, userview has references to posts and users. Post has_one userview, has_many users through userview, userview has_many posts and has_many users.
create_table "posts", force: :cascade do |t|
t.integer "userview_id"
t.bigint "userviews_id"
t.index ["userviews_id"], name: "index_posts_on_userviews_id"
end
create_table "userviews", force: :cascade do |t|
t.bigint "users_id"
t.bigint "posts_id"
t.integer "post_id"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["posts_id"], name: "index_userviews_on_posts_id"
t.index ["users_id"], name: "index_userviews_on_users_id"
end
class User < ActiveRecord::Base
has_many :viewed_posts, through: :userview, class_name: 'Post'
...
class Userview < ApplicationRecord
belongs_to :post
belongs_to :user
end
class Post < ApplicationRecord
belongs_to :user
has_many :userviews
has_many :viewers, through: :userview, class_name: 'User'
...
Could not find the source association(s) "viewer" or :viewers in model Userview. Try 'has_many :viewers, :through => :userviews, :source => <name>'. Is it one of post or user?
Although, that's for unless #post.viewers.include?(current_user)
Not sure if I understood correctly what are you trying to achieve, but I'll try to fix your associations.
To associate 2 models you need to store foreign key in one of them. Model which stores the key belongs_to other model. Eg in posts table you have user_id column, it means Post belongs_to: user and User has_many :posts (or has_one :post if you need one-to-one association). For such association you can write:
user = User.first
post = Post.last
user.posts << post
If you want all this stuff to work automatically you should follow the convention about naming. Foreign keys should be in singular form, like user_id, not users_id
If you want many-to-many association you need to create an intermediate table, which stores both foreign keys. It can be done using simple direct has_and_belongs_to_many association or with more complex has_many through:
I suppose that in your case it should be:
class User < ActiveRecord::Base
has_many :userviews
# it is posts viewed by the user
# you need to specify class name because it differs from association name
has_many :viewed_posts, through: :userviews, class_name: 'Post'
# posts written by the user
has_many :posts
end
class Userview < ApplicationRecord
belongs_to :post
belongs_to :user
end
class Post < ApplicationRecord
belongs_to :user
has_many :userviews
has_many :viewers, through: :userviews, source: :user
end
It means that you need post_id and user_id columns in userviews table and user_id in posts table. Please, remove useless columns and add needed in migration. When you set it all correctly you'll be able to do
#post.viewers << current_user
to add current_user to viewers list. Corresponding userview instance is created automagically
Changed has_many :viewers, through: :userviews, class_name: 'User'
to has_many :viewers, through: :userviews, source: :user
Problem
Okay, so I have a weird structure issue, and the rails conventions I know are not helping.
In my system, Users have Memberships to Servers. This is generically a HABTM association, but I am using has_many through the Memberships model to simplify some things (according to advice from http://blog.flatironschool.com/why-you-dont-need-has-and-belongs-to-many/). Memberships have metadata about the user's relationship with the server. This works pretty well, for what I am doing.
Now, the trouble I run into is that each Membership can have many Profiles for the User to use on the Server. Since Profiles are associated to one User and one Server, I just have the Profiles belong_to Membership. What I want to do is use a counter_cache for the Profiles on the Server model since Rails seems to be running this query constantly:
SELECT COUNT(*) FROM "profiles" INNER JOIN "memberships" ON "profiles"."membership_id" = "memberships"."id" WHERE "memberships"."server_id" = $1
My Attempts
Firstly, Rails does not have a belongs_to through, so profiles cannot "belong_to" servers through the membership model, thus I cannot provide the counter_cache there.
My second thought was to use a delegate on the Server model to delegate :profiles_count to Membership and just do the counter_cache via memberships, but I haven't been able to get this to work.
My third attempt was to delegate :server_id to :membership in Profile and see if that would allow me to build a belongs_to association with the Server model. No good. Funny thing here, there are no errors thrown for this, it just rolls back a save on a new profile without warning.
The only other thing I can think is to add server_id and user_id columns to the Profiles model effectively duplicating the HABTM association from Memberships. Im not sure how stable this level of interconnectedness is going to be. This is effectively going to create has_many associations in two directions, with circular references chains: membership->server->profile->membership....
Question
Is there a specific convention I have missed that handles counter_caching in this fashion, or am I stuck with a choice between:
the status quo of constantly querying the count from the database through an inner join
risking an infinite loop of associations
Supporting Code
Here are the pertinent classes trimmed down to the relevant bits:
class User < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :servers, through: :memberships
has_many :profiles, through: :memberships
end
class Server < ApplicationRecord
has_many :memberships
has_many :users, through: :memberships
has_many :profiles, through: :memberships
# This did not work with either singular or plural membership:
# delegate :profiles_count, to: :membership
end
class Profile < ApplicationRecord
belongs_to :membership, counter_cache: true
# The following also does not work:
# belongs_to :server, counter_cache: true
# delegate :server_id, to: :membership
scope :for_user, -> (id) { joins(:membership).where(memberships: { user_id: id }) }
scope :for_server, -> (id) { joins(:membership).where(memberships: { server_id: id}) }
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :server, counter_cache: true
has_many :profiles, dependent: :destroy
end
Here are the create_tables from db/schema.rb truncated down to just the associated fields for brevity:
create_table "users", force: :cascade do |t|
# ...
end
create_table "servers", force: :cascade do |t|
# ...
t.integer "memberships_count", default: 0
t.integer "profiles_count", default: 0
end
create_table "profiles", force: :cascade do |t|
t.bigint "membership_id"
# ...
end
create_table "memberships", force: :cascade do |t|
t.bigint "user_id"
t.bigint "server_id"
# ...
t.integer "profiles_count", default: 0
end
I've discovered a nice way to generate join tables for my HABTM relationships in my Rails app.
rails g migration CreateJoinTable table1 table2
This generates an ActiveRecord::Migration that employs the method create_join_table
I'm wondering what this wonderful mysterious method does. I guess it makes a table (probably without an id field) that has a column for table1 foreign key and a column for table2 foreign key, but does the table have any other features?. My habit for join tables has always been to add a unique index across both those columns so that a relationship between a record in table1 and a record in table2 cannot be entered twice.
My question boils down to: If I use create_join_table do I need to keep adding that unique index, or does this method do that for me (I think it should)?
The documentation I usually look at doesn't go into this sort of detail.
Called without any block, create_join_table just creates a table with two foreign keys referring to the two joined tables.
However, you can actually pass a block when you call the method to do any additional operations (say, adding indexes for example). From the Rails doc:
create_join_table :products, :categories do |t|
t.index :product_id
t.index :category_id
end
Have a look at create_join_table documentation.
You can check the create_join_table code at the bottom (click on Source: show).
SchemaStatements#create_join_table() only creates join table without any fancy indexes etc,... So if you wish to use uniqueness constraint on two fields you have to do something like this:
class CreateJoinTable < ActiveRecord::Migration
def change
create_join_table :posts, :users do |t|
t.integer :post_id, index: true
t.integer :user_id, index: true
t.index [:post_id, :user_id], name: 'post_user_un', unique: true
end
end
end
Please also note that create_join_table by default does NOT create id field.
It turns out it doesn't do any more than the basics I described in the question. I found this out simply by running the migration and seeing what ends up in db/schema.rb
For those interested, to get the unique index do this:
class CreateJoinTable < ActiveRecord::Migration
def change
create_join_table :posts, :users
add_index :posts_users, [:post_id, :user_id], unique: true, name: 'index_posts_users'
end
end
Also be aware of how you define the dependent destroy for this join table.
If you later move away from HABTM and define the relationships using through: and get it wrong you might run into the 'to_sym' error I reported here.
Make sure you have defined the destroy like this:
class Proposal < ActiveRecord::Base
has_many :assignments
has_many :products, through: :assignments, dependent: :destroy # <- HERE
end
class Product < ActiveRecord::Base
has_many :assignments
has_many :proposals, through: :assignments, dependent: :destroy # <- HERE
end
class Assignment < ActiveRecord::Base
belongs_to :product
belongs_to :proposal
end
not this:
class Proposal < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :products, through: :assignments
end
class Product < ActiveRecord::Base
has_many :assignments, dependent: :destroy
has_many :proposals, through: :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :product
belongs_to :proposal
end
All I follow the tutorial to make a simple many to many example,
http://hadiyahdotme.wordpress.com/2011/09/25/many-ways-to-do-many-to-many-hacker-notes/
I thought we only have to create another table "categorization " to make many-to-many relations between category and product model is enough!
why should we do 'rails generate migration create_categories_products_join'
I can not understand, any idea can help me , thanks a lot!
Between category and product must be association (connection) so You must create special table for that.
If You want use has_and_belongs_to_many:
in models/product.rb:
...
has_and_belongs_to_many :categories, :join_table => :categories_products
...
in models/category.rb:
...
has_and_belongs_to_many :products, :join_table => :categories_products
...
in your migration file create_categories_products_join.rb
...
create_table :categories_products, :id => false do |t|
t.references :product
t.references :category
end
add_index :categories_products, [:product_id, :category_id]
add_index :categories_products, [:category_id, :product_id]
...
and remove categorization model rails d model categorization
If You want use has_many :through
in models/product.rb:
...
has_many :categorizations
has_many :categories, :through => :categorizations
...
in models/category.rb:
...
has_many :categorizations
has_many :products, :through => :categorizations
...
in models/categorization.rb
...
belongs_to :product
belongs_to :category
...
in migration file create_categorizations
...
create_table :categorizations do |t|
t.references :product
t.references :category
#...
t.timestamps
end
add_index :categorizations, :category_id
add_index :categorizations, :product_id
...
migration create_categories_products_join You can remove rails d migration create_categories_products_join
Its just two examples. First one is about has_and_belongs_to_many second one is about has_many :through.
If you are going with has_many :through and categorization table there is no need to create another table categories_products.
Choose one of the methods and you are done. I would recommend has_many :through. Its more flexible.
When you have has and belongs to many (HABTM) relation you have to define a way to connect products to categories and vise versa. Thus, you should use join table.
In rails, when you code:
has_many :products, through: :categorizations
You should create a join table categorizations with reference to each one:
create_table "categorizations" do |t|
t.integer "category_id"
t.integer "product_id"
end
Then, you can access categories on specific product product.categories and products from category category.products
I suggest you to watch this RailsCast and review this code
Now creating join table in Rails 4 is cool see available methods -
Migration method to create_join_table creates a HABTM join table. A typical use would be:
create_join_table :products, :categories
You can pass the option :table_name when you want to customize the table name. For example:
create_join_table :products, :categories, table_name: :categorization
will create a categorization table.
create_join_table also accepts a block, which you can use to add indices (which are not created by default) or additional columns:
create_join_table :products, :categories do |t|
t.index :product_id
t.index :category_id
end
click here for more details
I'm really new to RoR so I apologize if I'm not thinking about this right. I have a Report where I need to be able to assign multiple users to that report. A user can be assigned to more than one report and a report can have multiple users. How do I create the database relationship where this would be allowed. I understand how to assign one user to one report but not many users to a single report.
I'd use a joining class to make this happen:
class Report
has_many :assignments
has_many :users :through => :assignments
end
class User
has_many :assignments
has_many :reports, :through => :assignments
end
class Assignment
belongs_to :report
belongs_to :user
end
The class Assignment has two fields: report_id and user_id to create the relationship.
Read the Ruby on Rails Guide to Active Record Associations: http://guides.rubyonrails.org/association_basics.html
I highly recommend you familiarize yourself with the Ruby on Rails guides. They will prove to be an invaluable asset!! For this task the site would be RailsGuides Active Record Associations.
As far as the code goes you want to create three database tables: reports, reports_users, and users, with reports_users being a join table.
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, :null => false
t.timestamps
end
end
end
class CreateReports < ActiveRecord::Migration
def change
create_table :reports do |t|
t.string :name, :null => false
t.timestamps
end
end
end
class ReportsUsers < ActiveRecord::Migration
def change
create_table :reports_users, :id => false do |t|
t.references :user, :null => false
t.references :report, :null => false
end
end
end
Once you run this migration you need to set up the active record associations in your models.
class User < ActiveRecord::Base
has_and_belongs_to_many :reports
end
class Report < ActiveRecord::Base
has_and_belongs_to_many :user
end
This will set up the database and the many-to-many models connections. This will get you started. Now you have to go create some views