コールバックの順番を確認

2021-09-14

rails, active_record

何度も気になって調べてるのでメモしとく

かたっぱしからコールバック仕込んで実行順番とか確認
family(0|1) - (n)member なモデルです

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Member < ApplicationRecord
  belongs_to :family, optional: true, touch: true

  validate :some_validation

  def some_validation
    errors.add('error') if name&.include?('hoge')
  end

  module CallbackCheck
    class << self
      def callback_kinds
        {
          before: %i[validation save create update destroy],
          after:  %i[validation save create update destroy commit rollback initialize find],
          around: %i[save create update destroy]
        }
      end

      def included(k)
        callback_kinds.each do |timing, actions|
          actions.each do |action|
            cb = "#{timing}_#{action}"
            cb_method = "callback_#{timing}_#{action}"
            k.define_method cb_method do |&block|
              puts cb
              block.call if block && timing == :around
            end
            k.send(cb.to_sym, cb_method.to_sym)
          end
        end
      end
    end
  end

  include CallbackCheck
end

普通にcreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> Member.create
after_initialize
before_validation
after_validation
before_save
around_save
before_create
around_create
  TRANSACTION (0.2ms)  BEGIN
  Member Create (0.3ms)  INSERT INTO `members` (`name`, `family_id`, ...
after_create
after_save
  TRANSACTION (6.2ms)  COMMIT
after_commit
=> #<Member:0x00007fb6dfa27090 id: 6, name: nil, family_id: nil, creat...

familyを指定(touch: true)

after_saveの後でタッチ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> Member.create(family: Family.first)
  Family Load (0.3ms)  SELECT `families`.* FROM `families` ORDER BY `families`.`id` ASC LIMIT 1
after_initialize
before_validation
after_validation
before_save
around_save
before_create
around_create
  TRANSACTION (0.3ms)  BEGIN
  Member Create (0.3ms)  INSERT INTO `members` (`name`, `family_id`...
after_create
after_save
  Family Update (0.5ms)  UPDATE `families` SET `families`.`updated_at` = ...
  TRANSACTION (5.6ms)  COMMIT
after_commit
=> #<Member:0x00007fb6df8b42d0 id: 7, name: nil, family_id: 1, created_at...

newしてsave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> m = Member.new
after_initialize
=> #<Member:0x00007fb6dfe42508 id: nil, name: nil, family_id: ni...
irb(main):010:0> m.save
before_validation
after_validation
before_save
around_save
before_create
around_create
  TRANSACTION (0.3ms)  BEGIN
  Member Create (0.4ms)  INSERT INTO `members` (`name`, `family_...
after_create
after_save
  TRANSACTION (9.9ms)  COMMIT
after_commit
=> true

newしてsave(invalid)

1
2
3
4
5
6
7
> m = Member.new(name: "hoge")
after_initialize
=> #<Member:0x00007fb6df9fcca0 id: nil, name: "hoge", family_id: n...
irb(main):012:0> m.save
before_validation
after_validation
=> false

findしてupdate

findでもafter_initialize走るのか。
まぁそりゃそうか。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> m = Member.find(1)
  Member Load (0.4ms)  SELECT `members`.* FROM `members` WHERE `members`.`id` = 1 LIMIT 1
after_find
after_initialize
=> #<Member:0x00007fb6dfb27be8 id: 1, name: "赤柴", family_id: nil, ...
irb(main):016:0> m.update(name: "foo")
before_validation
after_validation
before_save
around_save
before_update
around_update
  TRANSACTION (0.3ms)  BEGIN
  Member Update (0.4ms)  UPDATE `members` SET `members`.`name` = 'foo'...
after_update
after_save
  TRANSACTION (4.8ms)  COMMIT
after_commit
=> true

変更してsave

違和感なし。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> m.name = 'bar'
=> "bar"
irb(main):021:0> m.save!
before_validation
after_validation
before_save
around_save
before_update
around_update
  TRANSACTION (0.3ms)  BEGIN
  Member Update (0.4ms)  UPDATE `members` SET `members`.`name` = 'bar', `members`.`updated_at` = '2023-02-10 08:10:12.901602' WHERE `members`.`id` = 1
after_update
after_save
  TRANSACTION (8.7ms)  COMMIT
after_commit
=> true

after_commitで再commitしちゃうようなケース

Memberに以下を追加

1
2
3
4
5
  after_commit :recommit

  def recommit
    update(name: self.name + "_recommit")
  end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
> m.name = 'baz'
=> "baz"
irb(main):003:0> m.save
before_validation
after_validation
before_save
around_save
before_update
around_update
  TRANSACTION (0.3ms)  BEGIN
  Member Update (0.4ms)  UPDATE `members` SET `members`.`name` = 'baz',...
after_update
after_save
  TRANSACTION (9.1ms)  COMMIT
after_commit
before_validation
after_validation
before_save
around_save
before_update
around_update
  TRANSACTION (0.2ms)  BEGIN
  Member Update (0.4ms)  UPDATE `members` SET `members`.`name` = 'baz_recommit',...
after_update
after_save
  TRANSACTION (2.9ms)  COMMIT
after_commit
before_validation
after_validation
before_save
around_save
before_update
around_update
  TRANSACTION (0.2ms)  BEGIN
  Member Update (0.2ms)  UPDATE `members` SET `members`.`name` = 'baz_recommit_recommit', ...
after_update
after_save
  TRANSACTION (3.2ms)  COMMIT
after_commit

以降nameの桁があふれるまでループしてエラー
やべぇ
モデル単体でこんなロジックが必要になるはずはない
機能で閉じたクラス作ってそのトランザクションの中で解決すべきだと改めて思った。
callbackに :if つけれるよとか同僚に言われたけど無駄に複雑すぎるしテストしにくいし良いこと無くない?

コメント

投稿する

投稿したコメントはご自身で削除できません

不適切なコメントと判断した場合は管理側で削除することがあります