同時実行になった時におかしくなっちゃう、というその点でずっと使えなかった #increment!
が同時実行を意識した処理になったよ!
嬉しい! 素敵!
rails4
irb(main):001:0> counter = Counter.create
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "counters" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2016-02-22 11:39:38.966295"], ["updated_at", "2016-02-22 11:39:38.966295"]]
(1.9ms) commit transaction
=> #<Counter id: 4, value: nil, created_at: "2016-02-22 11:39:38", updated_at: "2016-02-22 11:39:38">
default 設定し忘れて nil になっているけれども value
は integer
です。
この value
を #increment!
したいと思います。
irb(main):004:0> counter.increment!(:value)
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ? [["value", 1], ["updated_at", "2016-02-22 11:40:48.595257"], ["id", 4]]
(2.7ms) commit transaction
=> true
実行された SQL を見ると value に 1 が指定されてるのがわかります。
この 1
は、どこからきたかっていうと、インスタンスがキャッシュしている value の値を元に計算されてるんですね。
つまりこれはどういうことかっていうと、
irb(main):005:0> counter1 = Counter.find(4)
Counter Load (0.2ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1 [["id", 4]]
=> #<Counter id: 4, value: 1, created_at: "2016-02-22 11:39:38", updated_at: "2016-02-22 11:40:48">
irb(main):006:0> counter2 = Counter.find(4)
Counter Load (0.1ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1 [["id", 4]]
=> #<Counter id: 4, value: 1, created_at: "2016-02-22 11:39:38", updated_at: "2016-02-22 11:40:48">
こんな感じで、同一レコードから生成された別のインスタンスがいる時に、それぞれで同時に #increment!
したらどうなるかってことです。
irb(main):007:0> counter1.increment!(:value)
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ? [["value", 2], ["updated_at", "2016-02-22 11:43:39.842789"], ["id", 4]]
(1.8ms) commit transaction
=> true
irb(main):008:0> counter2.increment!(:value)
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ? [["value", 2], ["updated_at", "2016-02-22 11:43:45.706247"], ["id", 4]]
(1.7ms) commit transaction
=> true
value
が 1 の状態から #increment!
は 2回呼ばれました。
value
が 3 になっていて欲しいのですが、実際には 2 です
irb(main):011:0> Counter.find(4).value
Counter Load (0.1ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1 [["id", 4]]
=> 2
この同時実行に #decrement!
が混ざってくると、もう何が何やらです。
irb(main):024:0> counter1.increment!(:value, 10)
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ? [["value", 12], ["updated_at", "2016-02-22 11:50:37.442805"], ["id", 4]]
(1.8ms) commit transaction
=> true
irb(main):025:0> counter2.increment!(:value, 20)
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ? [["value", 22], ["updated_at", "2016-02-22 11:50:52.249161"], ["id", 4]]
(1.7ms) commit transaction
=> true
irb(main):026:0> counter1.decrement!(:value, 10)
(0.1ms) begin transaction
SQL (0.3ms) UPDATE "counters" SET "value" = ?, "updated_at" = ? WHERE "counters"."id" = ? [["value", 2], ["updated_at", "2016-02-22 11:51:04.072007"], ["id", 4]]
(2.7ms) commit transaction
=> true
irb(main):027:0> Counter.find(4).value
Counter Load (0.1ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT 1 [["id", 4]]
=> 2
20 はどこに消えた
rails5
さあ、同じように Counter インスタンスを作ります。
irb(main):001:0> counter = Counter.create
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "counters" ("created_at", "updated_at") VALUES (?, ?) [["created_at", 2016-02-22 11:47:23 UTC], ["updated_at", 2016-02-22 11:47:23 UTC]]
(2.9ms) commit transaction
=> #<Counter id: 5, value: nil, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
おもむろに #increment!
しましょう。
irb(main):002:0> counter.increment!(:value)
SQL (3.2ms) UPDATE "counters" SET "value" = COALESCE("value", 0) + 1 WHERE "counters"."id" = ? [["id", 5]]
=> #<Counter id: 5, value: 1, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
発行された SQL を見ると UPDATE 文が変わったのがわかるかと思います。
"value" = COALESCE("value", 0) + 1
となっていますね(嬉
先ほどと同じように、同一のレコードから生成されたインスタンスを複数作って操作してみます。
irb(main):003:0> counter1 = Counter.find(5)
Counter Load (0.2ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
=> #<Counter id: 5, value: 1, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):004:0> counter2 = Counter.find(5)
Counter Load (0.2ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
=> #<Counter id: 5, value: 1, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):005:0> counter1.increment!(:value)
SQL (2.1ms) UPDATE "counters" SET "value" = COALESCE("value", 0) + 1 WHERE "counters"."id" = ? [["id", 5]]
=> #<Counter id: 5, value: 2, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):006:0> counter2.increment!(:value)
SQL (2.1ms) UPDATE "counters" SET "value" = COALESCE("value", 0) + 1 WHERE "counters"."id" = ? [["id", 5]]
=> #<Counter id: 5, value: 2, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):007:0> Counter.find(5).value
Counter Load (0.2ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
=> 3
今度はちゃんと value が 3 になっています
#decrement!
だって大丈夫!
irb(main):008:0> counter1.increment!(:value, 10)
SQL (2.2ms) UPDATE "counters" SET "value" = COALESCE("value", 0) + 10 WHERE "counters"."id" = ? [["id", 5]]
=> #<Counter id: 5, value: 12, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):009:0> counter2.increment!(:value, 20)
SQL (2.2ms) UPDATE "counters" SET "value" = COALESCE("value", 0) + 20 WHERE "counters"."id" = ? [["id", 5]]
=> #<Counter id: 5, value: 22, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):010:0> counter1.decrement!(:value, 10)
SQL (2.2ms) UPDATE "counters" SET "value" = COALESCE("value", 0) - 10 WHERE "counters"."id" = ? [["id", 5]]
=> #<Counter id: 5, value: 2, created_at: "2016-02-22 11:47:23", updated_at: "2016-02-22 11:47:23">
irb(main):011:0> Counter.find(5).value
Counter Load (0.2ms) SELECT "counters".* FROM "counters" WHERE "counters"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
=> 23
ただし #increment!
や #decrement!
した後は、「実際に DB に保存されている値」と「インスタンスのキャッシュ」にズレが出ている可能性があるというのは、意識しておいた方が良さそうです。
irb(main):012:0> counter1.value
=> 2
irb(main):013:0> counter2.value
=> 22