How to change a reference column to be polymorphic? - ruby-on-rails

My model (Bar) already has a reference column, let's call it foo_id and now I need to change foo_id to fooable_id and make it polymorphic.
I figure I have two options:
Create new reference column fooable which is polymorphic and migrate the ID's from foo_id (What would be the best way to migrate these? Could I just do Bar.each { |b| b.fooable_id = b.foo_id }?
Rename foo_id to fooable_id and add polymorphic to fooable_id. How to add polymorpic to an existing column?

1. Change the name of foo_id to fooable_id by renaming it in a migration like:
rename_column :bars, :foo_id, :fooable_id
2. and add polymorphism to it by adding the required foo_type column in a migration:
add_column :bars, :fooable_type, :string
3. and in your model:
class Bar < ActiveRecord::Base
belongs_to :fooable,
polymorphic: true
end
4. Finally seed the type of you already associated type like:
Bar.update_all(fooable_type: 'Foo')
Read Define polymorphic ActiveRecord model association!

Update for Rails >= 4.2
TLDR
add new reference
copy reference ids
remove old reference
So the migration is:
def change
add_reference :bars, :fooable, polymorphic: true
reversible do |dir|
dir.up { Bar.update_all("fooable_id = foo_id, fooable_type='Foo'") }
dir.down { Bar.update_all('foo_id = fooable_id') }
end
remove_reference :bars, :foo, index: true, foreign_key: true
end
Background
Current Rails generates references with index and foreign_key, which is a good thing.
This means that the answer of #Christian Rolle is no longer valid as after renaming foo_id it leaves a foreign_key on bars.fooable_id referencing foo.id which is invalid for other fooables.
Luckily, also the migrations evolve, so undoable migrations for references do exist.
Instead of renaming the reference id, you need to create a new reference and remove the old one.
What's new is the need to migrate the ids from the old reference to the new one.
This could be done by a
Bar.find_each { |bar| bar.update fooable_id: bar.foo_id }
but this can be very slow when there are already many relations. Bar.update_all does it on database level, which is much faster.
Of course, you should be able to roll back the migration, so when using foreign_keys the complete migration is:
def change
add_reference :bars, :fooable, polymorphic: true
reversible do |dir|
dir.up { Bar.update_all("fooable_id = foo_id, fooable_type='Foo'") }
dir.down { Bar.update_all('foo_id = fooable_id') }
end
remove_reference :bars, :foo, index: true, foreign_key: true
end
Remember that during rollback, change is processed from bottom to top, so foo_id is created before the update_all and everything is fine.

One small change I would make is to the migration:
#db/migrate/latest.rb
class Latest
def change
rename_column :bars, :foo_id, :fooable_id
add_column :bars, :fooable_type, :string, after: :id, default: 'Foo'
end
end
This would eliminate the need to do a data migration.
Update: this will work on rails 3 and up. According to the question the original base class is implied to be Foo.

In reference to your question specifically, here's what I'd do:
All the data in your Bar model is going to be stored in reference to the Bar model. This means that if you change the foo_id attribute in your model, you'll be able to just populate the bar_type attribute you need to add (as they'll all be able to reference the same model)
The way to do this is as follows:
Create migration for foo_id > fooable_id
Insert a fooable_type column
In rails console, loop through all existing records of Bar, filling the fooable_type column
First things first:
$ rails g migration ChangeFooID
#db/migrate/latest.rb
class Latest
def change
rename_column :bars, :foo_id, :fooable_id
add_column :bars, :fooable_type, :string, after: :id
end
end
This will create the various columns for you. Then you just need to be able to cycle through the records & change the type column:
rails c
Bar.find_each do |bar|
bar.update(barable_type: "Foo")
end
This will allow you to change the type of your columns, giving you the ability to associate all the current records with the respective records.
Polymorphism
You'll be able to use the Rails docs as a reference as to how to associate your models:
#app/models/foo.rb
class Foo < ActiveRecord::Base
has_many :bars, as: :barable
end
#app/models/bar.rb
class Bar < ActiveRecord::Base
belongs_to :foo, polymorphic: true
end

Related

after adding associations and index do I need to explicitly use foreign_key keyword to make a column foreign key in ruby-on-rails app

I need to need to make a column as foreign key.
I did lot of research on it.
I understood that I need to add associations. I am clear on belongs_to , has_one and has_many options.
After creating associations rails know that there is a foreign key association.If I remove the main record then dependent record will be deleted by rails app.
I was reading on migrations and I came across http://edgeguides.rubyonrails.org/active_record_migrations.html
where it is mentioned:
$ bin/rails generate migration AddUserRefToProducts user:references
will generate:
class AddUserRefToProducts < ActiveRecord::Migration
def change
add_reference :products, :user, index: true, foreign_key: true
end
end
Now on website: http://guides.rubyonrails.org/active_record_migrations.html
where it is mentioned:
$ bin/rails generate migration AddUserRefToProducts user:references
will generate:
class AddUserRefToProducts < ActiveRecord::Migration
def change
add_reference :products, :user, index: true
end
end
I understand the creation of index.
Do I need to have foreign_key: true explicitly or not? what difference will it make?
If you specify the foreign key in your migration, Rails will add a foreign key constraint at the database level. If you leave it out, no constraint will be added and it will behave like all previous versions of Rails e.g. you can delete records with the risk of orphaned records being left behind.
You can do something like
class AddUserRefToProducts < ActiveRecord::Migration
def change
add_reference :products, :user, index: true
add_foreign_key :products, :user
end
end
Similar solution here
First of all according to my knowledge http://edgeguides.rubyonrails.org/active_record_migrations.html is yet to be released. so the change foreign_key: true in your migration is not applicable yet so it wont make difference.
See this http://edgeguides.rubyonrails.org/index.html.
For stable versions, use http://guides.rubyonrails.org/
Now in http://guides.rubyonrails.org/active_record_migrations.html there is no option like foreign_key: true.
Even if you pass it, it wont make difference because according to the method add_reference
http://apidock.com/rails/v4.0.2/ActiveRecord/ConnectionAdapters/SchemaStatements/add_reference
It is not expecting foreign_key to be as option.
And lastly add_reference is basically calling http://apidock.com/rails/v4.0.2/ActiveRecord/ConnectionAdapters/SchemaStatements/add_column add_column method in it, which is also not expecting foreign_key to be passed as parameter, so it is not at all necessary.
Hope it makes sense
Actually, you don't need the option if you defined the association on the model. You migration could look like this:
class AddUserRefToProducts < ActiveRecord::Migration
def change
add_column :products, :user_id, :integer
end
end
If the Product model has the association, it should work
class Product < ActiveRecord::Base
belongs_to :user
end

ActiveRecord adding rating range in migration file

class AddRatingToBooks < ActiveRecord::Migration
def up
add_column :books, :rating, :integer
end
def down
remove_column :books, :rating
end
I have the following snippet of code in my db/migrate/, I'm trying to add ratings to my books table, where it would be in a range from 0-100, but I'm not sure how to add that here, all i could find was querying with ranges. I'm sure it's simple I'm just not there yet.
You don't need to specify the range of integer values in your migration file. The migration file is simply used to add the database column to store the rating. This is not the place to add validations.
You should use your Book model to specify a validation that ensures your ratings fall within a certain range. Something like this:
class Book < ActiveRecord::Base
validates :rating, :inclusion => { :in => 0..100 }
end
I would highly recommend reading the Rails guides on both migrations and validations.
Probably I'm too late with the answer. But it's possible to define validation on db level with Migration Validators project: https://github.com/vprokopchuk256/mv-core
As example, in your migration:
def change
change_table :books do |t|
t.integer :rating, inclusion: 0..100
end
end
and then in your model:
class Book < ActiveRecord::Base
enforce_migration_validations
end
As result your validation will be defined both in db ( as statement inside trigger or check constraint, depending on your db) and on your model
SQL ( PostgreSQL ):
=# insert into books(rating) values(10);
INSERT 0 1
=# insert into books(rating) values(200);
ERROR: new row for relation "books" violates check constraint "chk_mv_books_rating"
Rails console:
Book.new(title: 10).valid?
=> true
Book.new(title: 200).valid?
=> false

Add a reference column migration in Rails 4

A user has many uploads. I want to add a column to the uploads table that references the user. What should the migration look like?
Here is what I have. I'm not sure if I should use (1) :user_id, :int or (2) :user, :references. I'm not even sure if (2) works. Just trying to do this the "rails" way.
class AddUserToUploads < ActiveRecord::Migration
def change
add_column :uploads, :user_id, :integer
end
end
Relevant question except for Rails 3. Rails 3 migrations: Adding reference column?
Rails 4.x
When you already have users and uploads tables and wish to add a new relationship between them.
All you need to do is: just generate a migration using the following command:
rails g migration AddUserToUploads user:references
Which will create a migration file as:
class AddUserToUploads < ActiveRecord::Migration
def change
add_reference :uploads, :user, index: true
end
end
Then, run the migration using rake db:migrate.
This migration will take care of adding a new column named user_id to uploads table (referencing id column in users table), PLUS it will also add an index on the new column.
UPDATE [For Rails 4.2]
Rails can’t be trusted to maintain referential integrity; relational databases come to our rescue here. What that means is that we can add foreign key constraints at the database level itself and ensure that database would reject any operation that violates this set referential integrity. As #infoget commented, Rails 4.2 ships with native support for foreign keys(referential integrity). It's not required but you might want to add foreign key(as it's very useful) to the reference that we created above.
To add foreign key to an existing reference, create a new migration to add a foreign key:
class AddForeignKeyToUploads < ActiveRecord::Migration
def change
add_foreign_key :uploads, :users
end
end
To create a completely brand new reference with a foreign key(in Rails 4.2), generate a migration using the following command:
rails g migration AddUserToUploads user:references
which will create a migration file as:
class AddUserToUploads < ActiveRecord::Migration
def change
add_reference :uploads, :user, index: true
add_foreign_key :uploads, :users
end
end
This will add a new foreign key to the user_id column of the uploads table. The key references the id column in users table.
NOTE: This is in addition to adding a reference so you still need to create a reference first then foreign key (you can choose to create a foreign key in the same migration or a separate migration file). Active Record only supports single column foreign keys and currently only mysql, mysql2 and PostgreSQL adapters are supported. Don't try this with other adapters like sqlite3, etc. Refer to Rails Guides: Foreign Keys for your reference.
Rails 5
You can still use this command to create the migration:
rails g migration AddUserToUploads user:references
The migration looks a bit different to before, but still works:
class AddUserToUploads < ActiveRecord::Migration[5.0]
def change
add_reference :uploads, :user, foreign_key: true
end
end
Note that it's :user, not :user_id
if you like another alternate approach with up and down method try this:
def up
change_table :uploads do |t|
t.references :user, index: true
end
end
def down
change_table :uploads do |t|
t.remove_references :user, index: true
end
end
Create a migration file
rails generate migration add_references_to_uploads user:references
Default foreign key name
This would create a user_id column in uploads table as a foreign key
class AddReferencesToUploads < ActiveRecord::Migration[5.2]
def change
add_reference :uploads, :user, foreign_key: true
end
end
user model:
class User < ApplicationRecord
has_many :uploads
end
upload model:
class Upload < ApplicationRecord
belongs_to :user
end
Customize foreign key name:
add_reference :uploads, :author, references: :user, foreign_key: true
This would create an author_id column in the uploads tables as the foreign key.
user model:
class User < ApplicationRecord
has_many :uploads, foreign_key: 'author_id'
end
upload model:
class Upload < ApplicationRecord
belongs_to :user
end
Just to document if someone has the same problem...
In my situation I've been using :uuid fields, and the above answers does not work to my case, because rails 5 are creating a column using :bigint instead :uuid:
add_reference :uploads, :user, index: true, type: :uuid
Reference: Active Record Postgresql UUID
[Using Rails 5]
Generate migration:
rails generate migration add_user_reference_to_uploads user:references
This will create the migration file:
class AddUserReferenceToUploads < ActiveRecord::Migration[5.1]
def change
add_reference :uploads, :user, foreign_key: true
end
end
Now if you observe the schema file, you will see that the uploads table contains a new field. Something like: t.bigint "user_id" or t.integer "user_id".
Migrate database:
rails db:migrate
Another syntax of doing the same thing is:
rails g migration AddUserToUpload user:belongs_to
class MigrationName < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
add_reference :uploads, :user, index: {algorithm: :concurrently}
end
end

Possible to add a column to an ActiveRecord model that is a foreign key?

I can easily create a scaffold or model in Rails with a field that is a reference (foreign key) to another model:
rails g model Cat owner:references
rails g scaffold Cat owner:references
But I can't seem to do the same for adding a column in a migration:
rails g migration AddOwnerToCats owner:references
What the above does is produce a migration file like this:
class AddOwnerToCats < ActiveRecord::Migration
def change
add_column :cats, :owner, :references
end
end
And when I try and run it with rake db:migrate, I get this:
SQLite3::SQLException: near "references": syntax error: ALTER TABLE "cats" ADD "owner" references
So is there a way to add a column that is a reference to another model? Or do I just have to do:
rails g migration AddOwnerToCats owner_id:integer
And then go into the migration and add an index for owner_id?
One way, which i can think of, and is a workaround is, simply create the column, and give the join condition in the model. i.e.
has_many :x, :class_name => y, :foreign_key => z
I know it's an old question, but as of Rails 5 (and perhaps earlier versions), the official docs have been updated:
rails generate migration AddOwnerRefToCats owner:references
will generate
class AddOwnerRefToCats < ActiveRecord::Migration[5.0]
def change
add_reference :cats, :owner, foreign_key: true
end
end
You can also add , index: true to create an index.

Rails 3 migrations: Adding reference column?

If I create a new rails 3 migration with (for example)
rails g migration tester title:tester user:references
, everything works fine...however if I add a column with something along the lines of:
rails g migration add_user_to_tester user:references
the reference field is not recognised. In short, the question is: how do I add a referencing column to a rails migration from the command line?
If you are using the Rails 4.x you can now generate migrations with references, like this:
rails generate migration AddUserRefToProducts user:references
like you can see on rails guides
EDIT: This is an outdated answer and should not be applied for Rails 4.x+
You don't need to add references when you can use an integer id to your referenced class.
I'd say the advantage of using references instead of a plain integer is that the model will be predefined with belongs_to and since the model is already created and will not be affected when you migrate something existing, the purpose is kind of lost.
So I would do like this instead:
rails g migration add_user_id_to_tester user_id:integer
And then manually add belongs_to :user in the Tester model
Please note that you will most likely need an index on that column too.
class AddUserReferenceToTester < ActiveRecord::Migration
def change
add_column :testers, :user_id, :integer
add_index :testers, :user_id
end
end
With the two previous steps stated above, you're still missing the foreign key constraint. This should work:
class AddUserReferenceToTester < ActiveRecord::Migration
def change
add_column :testers, :user_id, :integer, references: :users
end
end
You can use references in a change migration. This is valid Rails 3.2.13 code:
class AddUserToTester < ActiveRecord::Migration
def change
change_table :testers do |t|
t.references :user, index: true
end
end
def down
change_table :testers do |t|
t.remove :user_id
end
end
end
c.f.: http://apidock.com/rails/ActiveRecord/ConnectionAdapters/SchemaStatements/change_table
Running rails g migration AddUserRefToSponsors user:references will generate the following migration:
def change
add_reference :sponsors, :user, index: true
end
When adding a column you need to make that column an integer and if possible stick with rails conventions. So for your case I am assuming you already have a Tester and User models, and testers and users tables.
To add the foreign key you need to create an integer column with the name user_id (convention):
add_column :tester, :user_id, :integer
Then add a belongs_to to the tester model:
class Tester < ActiveRecord::Base
belongs_to :user
end
And you might also want to add an index for the foreign key (this is something the references already does for you):
add_index :tester, :user_id
That will do the trick:
rails g migration add_user_to_tester user_id:integer:index
You can add references to your model through command line in the following manner:
rails g migration add_column_to_tester user_id:integer
This will generate a migration file like :
class AddColumnToTesters < ActiveRecord::Migration
def change
add_column :testers, :user_id, :integer
end
end
This works fine every time i use it..
For Rails 4
The generator accepts column type as references (also available as belongs_to).
This migration will create a user_id column and appropriate index:
$ rails g migration AddUserRefToProducts user:references
generates:
class AddUserRefToProducts < ActiveRecord::Migration
def change
add_reference :products, :user, index: true
end
end
http://guides.rubyonrails.org/active_record_migrations.html#creating-a-standalone-migration
For Rails 3
Helper is called references (also available as belongs_to).
This migration will create a category_id column of the appropriate type. Note that you pass the model name, not the column name. Active Record adds the _id for you.
change_table :products do |t|
t.references :category
end
If you have polymorphic belongs_to associations then references will add both of the columns required:
change_table :products do |t|
t.references :attachment, :polymorphic => {:default => 'Photo'}
end
Will add an attachment_id column and a string attachment_type column with a default value of Photo.
http://guides.rubyonrails.org/v3.2.21/migrations.html#creating-a-standalone-migration

Resources