Railsにはタイムスタンプカラム( created_at
/ updated_at
)があり、各カラムは
- データ作成時
created_at
と updated_at
が設定される
- データ更新時
という挙動になります。
2.2 スキーマのルール | Active Record の基礎 - Railsガイド
そんな中、「同じ値でデータ更新をした場合、 updated_at
は更新されない」と同僚より教わったため、メモを残します。
目次
環境
なお、今回使うモデルは、以下のコマンドで生成したものとします。
$ 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
メソッドを組み合わせた
f3.assign_attributes(name: 'シナノゴールド')
f3.has_changes_to_save? ? f3.save : f3.touch
にて実現できそうでした。
同じ値でデータ更新
こちらの場合は、 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)