86

I have a table in a Rails app with hundreds of thousands of records, and they only have a created_at timestamp. I'm adding the ability to edit these records, so I want to add an updated_at timestamp to the table. In my migration to add the column, I want to update all rows to have the new updated_at match the old created_at, since that's the default for newly created rows in Rails. I could do a find(:all) and iterate through the records, but that would take hours because of the size of the table. What I really want to do is:

UPDATE table_name SET updated_at = created_at;

Is there a nicer way to do that in a Rails migration using ActiveRecord rather than executing raw SQL?

Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
jrdioko
  • 32,230
  • 28
  • 81
  • 120

8 Answers8

160

I would create a migration

rails g migration set_updated_at_values

and inside it write something like:

class SetUpdatedAt < ActiveRecord::Migration
  def self.up
    Yourmodel.update_all("updated_at=created_at")
  end

  def self.down
  end
end

This way you achieve two things

  • this is a repeatable process, with each possible deploy (where needed) it is executed
  • this is efficient. I can't think of a more rubyesque solution (that is as efficient).

Note: you could also run raw sql inside a migration, if the query gets too hard to write using activerecord. Just write the following:

Yourmodel.connection.execute("update your_models set ... <complicated query> ...")
Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
nathanvda
  • 49,707
  • 13
  • 117
  • 139
  • +1 - I recently had to do exactly this and used SQL over an ActiveRecord. It's as fast as it can get. – Peter Brown Mar 08 '11 at 01:04
  • 43
    `Yourmodel.update_all 'update_at=created_at'` is nicer, no? It works on a scope too. – Marc-André Lafortune May 29 '13 at 20:05
  • According to the [Rails guide](https://guides.rubyonrails.org/active_record_migrations.html#using-the-up-down-methods): *"the database schema should be unchanged if you do an `up` followed by a `down`"*. So consider `def change` only instead. – EliadL Feb 13 '20 at 10:24
  • 1
    @EliadL a few remarks: 1) we are not changing the schema, just the contents of the database. And 2) at the time this answer was written, the `change` method did not yet exist, but in this case I still prefer to use explicit `up` and `down` to be more explicit (if you would want to control what the `down` should do). – nathanvda Feb 15 '20 at 17:16
  • I think your down method should put things back to how they were before the up method was run, whether it's structure or content of the database. If you can't figure out how to do that then leave the down method out and make it not able to roll back – Toby 1 Kenobi Mar 30 '22 at 04:45
  • Good suggestion. In my own case I generally add a (generic) column to all tables, named `migration_id` which I can set during a migration, to be able to rollback the migration later if ever needed. – nathanvda Apr 07 '22 at 09:44
  • Best practice says do NOT use Models within migrations. Contrary to everywhere else in a Rails app, it's better to use raw SQL here. Three years from now, `Yourmodel` might not exist in the code, perhaps renamed to `Usermodel` or whatever two years in. At that point, a three-year-old migration will fail, and a new hire will have to become an archeologist to fix it. However, at the point this migration runs, the `yourmodels` table *will* exist, regardless of the state of the application code in this-is-the-future code. – David Hempy Dec 13 '22 at 20:59
  • I understand that, but why not use the power of rails? It is easily fixed by locally defining the `YourModel` class (no code needed). I have used this in the rare case where we had a big data model redesign which you do in separate steps locally, but on a production machine there will be the mismatch you mentioned, between the code and db-model. But running a 3yo migration? I always use `rake db:setup` on a dev machine, which uses the schema instead of the migrations to build the db-model (and leave the migrations as documentation). – nathanvda Dec 31 '22 at 16:09
21

You can use update_all which works very similar to raw SQL. That's all options you have.

BTW personally I do not pay that much attention to migrations. Sometimes raw SQL is really best solution. Generally migrations code isn't reused. This is one time action so I don't bother about code purity.

Greg Dan
  • 6,198
  • 3
  • 33
  • 53
  • 2
    That depends on your deployment needs. I really like to use migrations, because they allow to repeatable deploy on existing platforms and get the same result. We have several stages of deploying: dev, test, qa/acceptance, a proof of concept platform (for testing clients), a production platform: we need to be able to migrate existing data to the newly deployed version without a fault. Adding a column and making sure the data is ok is in our case NOT a one time action. – nathanvda Mar 07 '11 at 21:35
  • I write about using `update_all` inside migration file :-) You can also execute raw SQL inside migration file. However `update_all` is a little bit more elegant. Both will perform exactly the same. – Greg Dan Mar 07 '11 at 21:47
  • 2
    It's usually a smart idea to declare the model in the migration, since this will prevent problems if the original model is redefined later on. Just found this article which explains everything quite nicely: http://complicated-simplicity.com/2010/05/using-models-in-rails-migrations/ – François Beausoleil Mar 08 '11 at 03:20
  • With `update_all` i have no idea how to set a value of column to that of another, as the OP requested. Please demonstrate. – nathanvda Mar 08 '11 at 06:55
19

As gregdan wrote, you can use update_all. You can do something like this:

Model.where(...).update_all('updated_at = created_at')

The first portion is your typical set of conditions. The last portion says how to make the assignments. This will yield an UPDATE statement, at least in Rails 4.

Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
Martin Streicher
  • 1,983
  • 1
  • 18
  • 18
  • This in 4.2 generates `SET'posts'.'email' = 'options'`, the options is a literal string – lulalala Oct 06 '15 at 13:21
  • Confirming this tip doesn't work for me too. Unsure that it's completely wrong solution. Don't donwvote @martin answer – woto Oct 16 '15 at 18:31
  • Here is the output from the Rails console: `User.update_all('updated_at = created_at')` `SQL (0.4ms) UPDATE "users" SET updated_at = created_at` – Martin Streicher Oct 17 '15 at 14:09
4

You can directly run following command to your rails console ActiveRecord::Base.connection.execute("UPDATE TABLE_NAME SET COL2 = COL1")

For example: I want to update sku of my items table with remote_id of items tables. the command will be as following:
ActiveRecord::Base.connection.execute("UPDATE items SET sku = remote_id")

Sarwan Kumar
  • 1,283
  • 9
  • 24
  • 1
    Actually this is far most "history" secure way, because in future (when some will run migrations, the model `Yourmodel` can be already deleted. Try to avoid using models in migrations. – Foton Sep 24 '19 at 12:33
3

Do not use application models in migrations unless you redefine them inside migration. If you use application model tahat you later change or delete your migration might fail.

Of course you can also use full power of SQL inside migrations.

Read https://makandracards.com/makandra/15575-how-to-write-complex-migrations-in-rails

1

You can also add updated_at column and update its values in one migration:

class AddUpdatedAtToTableName < ActiveRecord::Migration
  def change
    add_column :table_name, :updated_at, :datetime

    reversible do |dir|
      dir.up do
        update "UPDATE table_name SET updated_at=created_at"
      end
    end
  end
end
Magdalena
  • 419
  • 4
  • 8
  • I consider this bad practice: I prefer to separate my migrations into 1) schema changes and 2) keep data changes separate. For clarity. – nathanvda Apr 07 '22 at 09:47
0

This is a General way of solving, without the need for Writing Query, as queries are subjected to risk.

  class Demo < ActiveRecord::Migration
    def change
     add_column :events, :time_zone, :string
     Test.all.each do |p|
       p.update_attributes(time_zone: p.check.last.time_zone)
     end
     remove_column :sessions, :time_zone
    end
  end
Wilson Varghese
  • 127
  • 1
  • 7
  • Please explain: why are queries subjected to risk? If you are changing deployment to a different database? – nathanvda Apr 07 '22 at 09:48
-5

As a one time operation, I would just do it in the rails console. Will it really take hours? Maybe if there are millions of records…

records = ModelName.all; records do |r|; r.update_attributes(:updated_at => r.created_at); r.save!; end;`
Deepak Mahakale
  • 22,834
  • 10
  • 68
  • 88
ghoppe
  • 21,452
  • 3
  • 30
  • 21
  • That's essentially what I tried first, but because there are hundreds of thousands of records that need to be changed, that will take hours (days?). – jrdioko Mar 07 '11 at 20:43
  • When I tested it it was going at about 50 records a second on my developer machine (not a server). – jrdioko Mar 07 '11 at 22:12
  • 4
    Always avoid iteration if possible, avoid using 'all' which loads every record into RAM at once, and since update_attributes already does a save automatically, the additional call to save! will make the entire operation take twice as long. – ryan0 Mar 10 '15 at 19:55