Railsにはタイムスタンプカラム( created_at
/ updated_at
)があり、各カラムは
- データ作成時
created_at
とupdated_at
が設定される
- データ更新時
updated_at
が更新される
という挙動になります。
2.2 スキーマのルール | Active Record の基礎 - Railsガイド
そんな中、「同じ値でデータ更新をした場合、 updated_at
は更新されない」と同僚より教わったため、メモを残します。
目次
環境
- Rails 7.0.4
なお、今回使うモデルは、以下のコマンドで生成したものとします。
$ rails g model Fruit name:string
Rails consoleで動作確認
Rails consoleで動作を確認してみます。
データ作成時
created_at
と updated_at
が設定されています。
# Rails consoleを起動 $ rails c Loading development environment (Rails 7.0.4) # データを作成 irb(main):001:0> Fruit.create(name: 'シナノゴールド') TRANSACTION (0.0ms) begin transaction Fruit Create (0.2ms) INSERT INTO "fruits" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "シナノゴ ールド"], ["created_at", "2022-11-21 11:55:32.998457"], ["updated_at", "2022-11-21 11:55:32.998457"]] TRANSACTION (8.2ms) commit transaction => #<Fruit:0x00007fabed572df8 id: 3, name: "シナノゴールド", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00>
同じ値でデータ更新
nameを シナノゴールド
から シナノゴールド
に更新してみたところ、SQLのUPDATE文が発行されていませんでした。
# データの取得 irb(main):002:0> f = Fruit.find(3) Fruit Load (0.2ms) SELECT "fruits".* FROM "fruits" WHERE "fruits"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] => #<Fruit:0x00007fabed4c93c0 # データの確認 irb(main):003:0> f => #<Fruit:0x00007fabed4c93c0 id: 3, name: "シナノゴールド", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00> # 同じ値でデータ更新しても、UPDATE文が発行されない irb(main):004:0> f.update(name: 'シナノゴールド') => true # 再度データを取得 irb(main):005:0> f2 = Fruit.find(3) Fruit Load (0.1ms) SELECT "fruits".* FROM "fruits" WHERE "fruits"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] => #<Fruit:0x00007fabed3d1378 # 中身を確認すると、updated_atはそのまま irb(main):006:0> f2 => #<Fruit:0x00007fabed3d1378 id: 3, name: "シナノゴールド", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00>
別の値でデータ更新
nameを シナノゴールド
から シナノスイート
に更新してみたところ、UPDATE文が発行され、 updated_at
も更新されていました。
# シナノゴールドからシナノスイートに更新すると、UPDATE文が発行される irb(main):007:0> f2.update(name: 'シナノスイート') TRANSACTION (0.1ms) begin transaction Fruit Update (0.2ms) UPDATE "fruits" SET "name" = ?, "updated_at" = ? WHERE "fruits"."id" = ? [["name", "シナノスイ ート"], ["updated_at", "2022-11-21 11:56:58.203100"], ["id", 3]] TRANSACTION (2.6ms) commit transaction => true # データの確認 irb(main):008:0> f3 = Fruit.find(3) Fruit Load (0.1ms) SELECT "fruits".* FROM "fruits" WHERE "fruits"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] => #<Fruit:0x00007fabed254798 ... irb(main):009:0> f3 => #<Fruit:0x00007fabed254798 id: 3, name: "シナノスイート", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 11:56:58.203100000 UTC +00:00>
同じ値で更新したときも、updated_atを更新したい場合
一方、「同じ値で更新したときも updated_at
を更新したい」場合はどうすればよいか調べたところ、 assign_attributes
と has_changes_to_save?
メソッドと touch
メソッドを組み合わせた
# saveメソッドを実行しない、 assign_attributesにて値を設定 f3.assign_attributes(name: 'シナノゴールド') # 変更があるときはsave、変更がないときはtouchを実行 f3.has_changes_to_save? ? f3.save : f3.touch
にて実現できそうでした。
- https://api.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html#method-i-assign_attributes
- 項目に値をセット、ただし保存はしない
- https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html#method-i-has_changes_to_save-3F
- モデルに変更がある場合は true
- https://api.rubyonrails.org/v7.0.4/classes/ActiveRecord/Persistence.html#method-i-touch
- updated_at を更新
同じ値でデータ更新
こちらの場合は、 updated_at
のみ更新されました。
# 同じ name を設定 irb(main):023:0> f3.assign_attributes(name: 'シナノゴールド') => nil # touchが動く irb(main):024:0> f3.has_changes_to_save? ? f3.save : f3.touch TRANSACTION (0.1ms) begin transaction Fruit Update (0.2ms) UPDATE "fruits" SET "updated_at" = ? WHERE "fruits"."id" = ? [["updated_at", "2022-11-21 14:10:03.451663"], ["id", 3]] TRANSACTION (7.9ms) commit transaction => true # updated_at が更新されていることを確認 irb(main):025:0> f3 => #<Fruit:0x00007f9c310f5198 id: 3, name: "シナノゴールド", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 14:10:03.451663000 UTC +00:00>
別の値でデータ更新
name
をシナノスイートからシナノゴールドへ変更してみたところ、 update
メソッドと同様の結果となりました。
# 現在の状態を確認 irb(main):019:0> f3 => #<Fruit:0x00007f9c310f5198 id: 3, name: "シナノスイート", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 14:06:35.333012000 UTC +00:00> # nameをシナノゴールドに変更 (この時点では保存しない) irb(main):020:0> f3.assign_attributes(name: 'シナノゴールド') => nil # saveが動く irb(main):021:0> f3.has_changes_to_save? ? f3.save : f3.touch TRANSACTION (0.1ms) begin transaction Fruit Update (0.2ms) UPDATE "fruits" SET "name" = ?, "updated_at" = ? WHERE "fruits"."id" = ? [["name", "シナノゴー ルド"], ["updated_at", "2022-11-21 14:07:47.869049"], ["id", 3]] TRANSACTION (8.2ms) commit transaction => true # nameとupdated_atが更新されていることを確認 irb(main):022:0> f3 => #<Fruit:0x00007f9c310f5198 id: 3, name: "シナノゴールド", created_at: Mon, 21 Nov 2022 11:55:32.998457000 UTC +00:00, updated_at: Mon, 21 Nov 2022 14:07:47.869049000 UTC +00:00>
参考:Djangoの auto_now_add と auto_now の場合
Djangoの場合、 auto_now_add
と auto_now
を使うことで、Railsの created_at
と updated_at
相当の処理ができます。
ただし、Djangoの auto_now
では、変更がない場合もタイムスタンプが更新される仕様です。
例えば、以下のモデルがあるとします。
from django.db import models class Fruit(models.Model): name = models.CharField('名前', max_length=255) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
このモデルに対して name
を同じ値で更新しても、 updated_at
は更新されます。
Django 4.1.4 の Django shellで試してみます。
# モデルの生成 >>> from myapp.models import Fruit >>> Fruit.objects.create(name='シナノゴールド') <Fruit: Fruit object (1)> >>> f = Fruit.objects.get(id=2) >>> f.created_at datetime.datetime(2022, 11, 21, 13, 9, 0, 156105, tzinfo=datetime.timezone.utc) >>> f.updated_at datetime.datetime(2022, 11, 21, 13, 9, 0, 156135, tzinfo=datetime.timezone.utc) # nameをシナノゴールドで更新 >>> f.name = 'シナノゴールド' >>> f.save() # created_atはそのままだが、updated_atは更新される >>> f.created_at datetime.datetime(2022, 11, 21, 13, 9, 0, 156105, tzinfo=datetime.timezone.utc) >>> f.updated_at datetime.datetime(2022, 11, 21, 13, 9, 55, 99880, tzinfo=datetime.timezone.utc)