Rationale of Rails 5 Join Table Generated Defaults - join

The Rails 5 command rails g migration create_foo_bar_join_table generates the following migration:
class CreateFooBarJoinTable < ActiveRecord::Migration[5.0]
def change
create_join_table :foos, :bars do |t|
# t.index [:foo_id, :bar_id]
# t.index [:bar_id, :foo_id]
end
end
end
Why does the generator stub out two (bi-directional) indices with composite keys? Also why are they commented out? I'm confused by this and can't find any clear explanation for having these suggested defaults.
Are the above indices more lookup efficient than the ones below?
...
create_join_table :foos, :bars do |t|
t.index :foo_id
t.index :bar_id
end
...

Stumbled on the exact answer reading the docs on has_and_belongs_to_many:
It’s also a good idea to add indexes to each of those columns to speed
up the joins process. However, in MySQL it is advised to add a
compound index for both of the columns as MySQL only uses one index
per table during the lookup.
https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many

Related

What side do you use Uniq in rails migration

In my rails app I have 2 models Profile and Skill.
Profile has_and_belongs_to_many Skill and can only have one time the same Skill.
Skill has_and_belongs_to_many Profile. If we respect the first relation, it should therefore not have more than once the same Profile.
When I create my join table I have two possibilities:
rails g migration CreateProfilesSkillsJoinTable profiles:uniq skills
or
rails g migration CreateProfilesSkillsJoinTable profiles skills:uniq
The first option will generate
class CreateProfilesSkillsJoinTable < ActiveRecord::Migration[5.1]
def change
create_join_table :profiles, :skills do |t|
t.index [:profile_id, :skill_id], unique: true
# t.index [:skill_id, :profile_id]
end
end
end
The second will generate
class CreateProfilesSkillsJoinTable < ActiveRecord::Migration[5.1]
def change
create_join_table :profiles, :skills do |t|
# t.index [:profile_id, :skill_id]
t.index [:skill_id, :profile_id], unique: true
end
end
end
You want to make the index unique :
add_index :something, [:profile_id, :skill_id], unique: true
First first rule is verified (you can get 1:2 only once). Note that even with an habtm, you'll tend to create your relation the same way (profile.skills << skill), you just need to ensure skill.profiles << profile does not creates unwanted relations

Rails create join table index name is too long

So I'm trying to create a join table between the tables users and looking_for_options.
This is my migration file:
class CreateJoinTableOptionsUsers < ActiveRecord::Migration[5.0]
def change
create_join_table :looking_for_options, :users do |t|
t.index [:looking_for_option_id, :user_id]
t.index [:user_id, :looking_for_option_id]
end
end
end
But I'm getting this error:
Index name 'index_looking_for_options_users_on_looking_for_option_id_and_user_id' on table 'looking_for_options_users' is too long; the lim
it is 64 characters
Knowing that for a join table, rails convention is Table_A_Name_Table_B_Name and its columns follow a similar convention Table_A_id and Table_B_id.
How do I specify a shorter column name for the joint table so it doesn't break rails many-to-many associations?
Update:
I found that I can give just the index a different name instead. But will rails's many-to-many association actually utilize it?
class CreateJoinTableOptionsUsers < ActiveRecord::Migration[5.0]
def change
create_join_table :looking_for_options, :users do |t|
t.index [:looking_for_option_id, :user_id], name: 'option_user'
t.index [:user_id, :looking_for_option_id], name: 'user_option'
end
end
end
... will rails's many-to-many association actually utilize it?
The choice of whether to use the index or not is made by the database optimiser, and is not affected by Rails. You can name it what you like, within the constraints imposed by the database.

Rails: populate an existing table via a migration

Say I have an existing migration in Rails:
class CreateStudies < ActiveRecord::Migration
def change
create_table :studies do |t|
t.string :display_name, null: false
t.string :tag_name, null: false
t.timestamps
end
add_index :studies, :tag_name, unique: true
end
end
Later on I realise that I would like to populate this table with a number of rows and I don't want to use rake db:rollback or the seeds.rb file. What is the format of the new migration file?
Just:
At the command prompt generate:
rails generate migration AddInitialStudies
And modify up and down method of generated migration:
class AddInitialStudies < ActiveRecord::Migration
def up
Study.create(display_name: "Example name", tag_name: "example_name")
end
def down
Study.delete_all
end
end
At this point I generate one Study object, but you can add as many you want.
On down, remove records added on up, because rollback and migrate again will duplicate added records. I assume the only Study records are the created on up. Take care with :tag_name, which must be unique.
I suppose that it's better to use rake tasks to generate some new objects rather than using migration. Here is a sample code https://github.com/maxmilan/Adverts_desk/blob/master/lib/tasks/advert_generators.rake#L3. You can also send number of generated objects as argument of task.

Rails has_and_belongs_to_many migration

I have two models restaurant and user that I want to perform a has_and_belongs_to_many relationship.
I have already gone into the model files and added the has_and_belongs_to_many :restaurants and has_and_belongs_to_many :users
I assume at this point I should be able to do something like with Rails 3:
rails generate migration ....
but everything I have tried seems to fail. I'm sure this is something really simple I'm new to rails so I'm still learning.
You need to add a separate join table with only a restaurant_id and user_id (no primary key), in alphabetical order.
First run your migrations, then edit the generated migration file.
Rails 3
rails g migration create_restaurants_users_table
Rails 4:
rails g migration create_restaurants_users
Rails 5
rails g migration CreateJoinTableRestaurantUser restaurants users
From the docs:
There is also a generator which will produce join tables if JoinTable
is part of the name:
Your migration file (note the :id => false; it's what prevents the creation of a primary key):
Rails 3
class CreateRestaurantsUsers < ActiveRecord::Migration
def self.up
create_table :restaurants_users, :id => false do |t|
t.references :restaurant
t.references :user
end
add_index :restaurants_users, [:restaurant_id, :user_id]
add_index :restaurants_users, :user_id
end
def self.down
drop_table :restaurants_users
end
end
Rails 4
class CreateRestaurantsUsers < ActiveRecord::Migration
def change
create_table :restaurants_users, id: false do |t|
t.belongs_to :restaurant
t.belongs_to :user
end
end
end
t.belongs_to will automatically create the necessary indices. def change will auto detect a forward or rollback migration, no need for up/down.
Rails 5
create_join_table :restaurants, :users do |t|
t.index [:restaurant_id, :user_id]
end
Note: There is also an option for a custom table name that can be passed as a parameter to create_join_table called table_name. From the docs
By default, the name of the join table comes from the union of the
first two arguments provided to create_join_table, in alphabetical
order. To customize the name of the table, provide a :table_name
option:
The answers here are quite dated. As of Rails 4.0.2, your migrations make use of create_join_table.
To create the migration, run:
rails g migration CreateJoinTableRestaurantsUsers restaurant user
This will generate the following:
class CreateJoinTableRestaurantsUsers < ActiveRecord::Migration
def change
create_join_table :restaurants, :users do |t|
# t.index [:restaurant_id, :user_id]
# t.index [:user_id, :restaurant_id]
end
end
end
If you want to index these columns, uncomment the respective lines and you're good to go!
When creating the join table, pay careful attention to the requirement that the two tables need to be listed in alphabetical order in the migration name/class. This can easily bite you if your model names are similar, e.g. "abc" and "abb". If you were to run
rails g migration create_abc_abb_table
Your relations will not work as expected. You must use
rails g migration create_abb_abc_table
instead.
For HABTM relationships, you need to create a join table. There is only join table and that table should not have an id column. Try this migration.
def self.up
create_table :restaurants_users, :id => false do |t|
t.integer :restaurant_id
t.integer :user_id
end
end
def self.down
drop_table :restaurants_users
end
You must check this relationship rails guide tutorials

A migration to add unique constraint to a combination of columns

What I need is a migration to apply unique constraint to a combination of columns. i.e. for a people table, a combination of first_name, last_Name and Dob should be unique.
add_index :people, [:firstname, :lastname, :dob], unique: true
According to howmanyofme.com, "There are 46,427 people named John Smith" in the United States alone. That's about 127 years of days. As this is well over the average lifespan of a human being, this means that a DOB clash is mathematically certain.
All I'm saying is that that particular combination of unique fields could lead to extreme user/customer frustration in future.
Consider something that's actually unique, like a national identification number, if appropriate.
(I realise I'm very late to the party with this one, but it could help future readers.)
You may want to add a constraint without an index. This will depend on what database you're using. Below is sample migration code for Postgres. (tracking_number, carrier) is a list of the columns you want to use for the constraint.
class AddUniqeConstraintToShipments < ActiveRecord::Migration
def up
execute <<-SQL
alter table shipments
add constraint shipment_tracking_number unique (tracking_number, carrier);
SQL
end
def down
execute <<-SQL
alter table shipments
drop constraint if exists shipment_tracking_number;
SQL
end
end
There are different constraints you can add. Read the docs
For completeness sake, and to avoid confusion here are 3 ways of doing the same thing:
Adding a named unique constraint to a combination of columns in Rails 5.2+
Let's say we have Locations table that belongs to an advertiser and has column reference_code and you only want 1 reference code per advertiser. so you want to add a unique constraint to a combination of columns and name it.
Do:
rails g migration AddUniquenessConstraintToLocations
And make your migration look either something like this one liner:
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
add_index :locations, [:reference_code, :advertiser_id], unique: true, name: 'uniq_reference_code_per_advertiser'
end
end
OR this block version.
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
change_table :locations do |t|
t.index ['reference_code', 'advertiser_id'], name: 'uniq_reference_code_per_advertiser', unique: true
end
end
end
OR this raw SQL version
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
execute <<-SQL
ALTER TABLE locations
ADD CONSTRAINT uniq_reference_code_per_advertiser UNIQUE (reference_code, advertiser_id);
SQL
end
end
Any of these will have the same result, check your schema.rb
Hi You may add unique index in your migration to the columns for example
add_index(:accounts, [:branch_id, :party_id], :unique => true)
or separate unique indexes for each column
In the typical example of a join table between users and posts:
create_table :users
create_table :posts
create_table :ownerships do |t|
t.belongs_to :user, foreign_key: true, null: false
t.belongs_to :post, foreign_key: true, null: false
end
add_index :ownerships, [:user_id, :post_id], unique: true
Trying to create two similar records will throw a database error (Postgres in my case):
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_ownerships_on_user_id_and_post_id"
DETAIL: Key (user_id, post_id)=(1, 1) already exists.
: INSERT INTO "ownerships" ("user_id", "post_id") VALUES ($1, $2) RETURNING "id"
e.g. doing that:
Ownership.create!(user_id: user_id, post_id: post_id)
Ownership.create!(user_id: user_id, post_id: post_id)
Fully runnable example: https://gist.github.com/Dorian/9d641ca78dad8eb64736173614d97ced
db/schema.rb generated: https://gist.github.com/Dorian/a8449287fa62b88463f48da986c1744a
If you are creating a new table just add unique: true
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title, unique: true
t.text :body
t.references :user, foreign_key: true
t.timestamps
end
add_index :posts, :user_id, unique: true
end
end

Resources