Railsにて、同じ値でデータ更新した場合、タイムスタンプカラム(updated_at)が更新されない

Railsにはタイムスタンプカラム( created_at / updated_at )があり、各カラムは

  • データ作成時
    • created_atupdated_at が設定される
  • データ更新時
    • updated_at が更新される

という挙動になります。
2.2 スキーマのルール | Active Record の基礎 - Railsガイド

 
そんな中、「同じ値でデータ更新をした場合、 updated_at は更新されない」と同僚より教わったため、メモを残します。

 
目次

 

環境

 
なお、今回使うモデルは、以下のコマンドで生成したものとします。

$ rails g model Fruit name:string

 

Rails consoleで動作確認

Rails consoleで動作を確認してみます。

 

データ作成時

created_atupdated_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_attributeshas_changes_to_save? メソッドと touch メソッドを組み合わせた

# saveメソッドを実行しない、 assign_attributesにて値を設定
f3.assign_attributes(name: 'シナノゴールド')

# 変更があるときはsave、変更がないときはtouchを実行
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_addauto_now を使うことで、Railscreated_atupdated_at 相当の処理ができます。

ただし、Djangoauto_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)