Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
25
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

@kano-e

Rails5 で `#increment!` と `#decrement!` が同時実行を意識した処理になった!

同時実行になった時におかしくなっちゃう、というその点でずっと使えなかった #increment! が同時実行を意識した処理になったよ!
嬉しい! 素敵!

rails4

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 になっているけれども valueinteger です。
この value#increment! したいと思います。

rails4
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 の値を元に計算されてるんですね。

つまりこれはどういうことかっていうと、

rails4
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! したらどうなるかってことです。

rails4
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 です :worried:

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! が混ざってくると、もう何が何やらです。

rails4
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 はどこに消えた :worried:

rails5

さあ、同じように Counter インスタンスを作ります。

rails5
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! しましょう。

rails5
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 となっていますね(嬉

先ほどと同じように、同一のレコードから生成されたインスタンスを複数作って操作してみます。

rails5
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 になっています :smile:

#decrement! だって大丈夫!

rails5
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 に保存されている値」と「インスタンスのキャッシュ」にズレが出ている可能性があるというのは、意識しておいた方が良さそうです。

rails5
irb(main):012:0> counter1.value
=> 2
irb(main):013:0> counter2.value
=> 22
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
25
Help us understand the problem. What are the problem?