• DoRubyとは
  • お問い合わせ
  • Ruby/Rails
  • Web開発
  • Webマーケティング
  • アピリッツ
  • ライフハック
  • ゲーム制作/開発
  • Railsの小技
  • Rubyの小技
  • Gemの紹介
  • ActiveRecord
  • 初心者向け
  • Java/Scalaテク
  • Unixのあれこれ
  • データベース
  • スマホ開発
  • HTML/CSS/JavaScript
  • デザイン製作
  • インフラ
  • クラウド
  • セキュリティ
  • エディタ
  • バージョン管理
  • その他
  • python
  • アクセス解析
  • Googleアナリティクス
  • Googleデータスタジオ
  • Web広告
  • SEO
  • UI/UX
  • ソーシャルメディア
  • EC開発
  • Webシステム開発
  • コンサルティング
  • Webデザイン
  • ブロックチェーン
  • ゲーム紹介
  • アプリ紹介
  • ASP
  • 風景
  • パソコン
  • ツール
  • ガジェット
  • 仕事術
  • 健康
  • 生活
  • 書評
  • Excel(エクセル)
  • PowerPoint(パワーポイント)
  • ゲームプランニング
  • SpriteStudio
  • マスターデータ入力/作成
  • Unity
  • キャラクターデザイン
  • ゲームシナリオ
  • レベルデザイン
  • ゲーム分析
  • 3DCG
  • イラスト制作
  • CG/アニメーション
  1. ホーム
  2. Ruby/Rails
  3. ActiveRecord
  4. ActiveRecordとデータベースについて知っておきたいこと
  • 2017-09-29
    • カテゴリ:
    • ActiveRecord
    • タグ:
    • データベース
    • Rails

ActiveRecordとデータベースについて知っておきたいこと

この記事は公開から1年以上が経過しています。情報が古い可能性がありますのでご注意ください。

Ruby on RailsのActiveRecordをぼーっと使っていると、メモリにあるデータやdbにあるデータの整合性が取れなくなってバグを生んでしまう事があるという話です。

ActiveRecordが好きなのでたびたびActiveRecordの話をしますが、今回はうっかりやってしまいがちなDB上のデータとメモリ上のデータの不整合との話です。知らないと失敗しがちなので

railsは5.1.1を使っていますが、あまりバージョンには関係なく、rails固有というわけでもなさそうな話です。

うっかりしてしまった!

プレイヤーが複数持つデッキの中から、一つだけメインデッキを選べるような実装がしたいとします。

class Player < ApplicationRecord
  has_many :decks
  has_one :selected_deck, -> { where(selected_flg: true) }, class_name: "Deck"

  def select_main_deck(deck)
    transaction do
      self.decks.each{ |deck| deck.update!(selected_flg: false) }
      deck.update!(selected_flg: true)
    end
  end
end

素直に読むと、一度すべてを非選択状態にし、選択したいデッキを選択しているようです。
色々問題はありますが、この select_main_deck が同時に使われることがないと仮定しても、
今メインデッキとして選択されているデッキをまた選択した場合、全てのデッキが非選択状態になってしまうのです。

※ ふつうeachではなくupdate_allを使うところですが、update_allにはまた別の落とし穴があり、本筋からそれるここではeachを使っています。

どうしてこうなってしまうのか

一瞬何が悪かったのかわかりにくいですが、

ひとつめ: DBで変更があったとしてもメモリ上のデータは更新されない

下の例では first_player が指すものと another_first_player が指すものはメモリ上では別の場所におかれており、
ActiveRecodr::Baseは勝手に変更を取ってきてはくれません。

first_player = Player.first # #<Player id: 1, name: "あああああ">
another_first_player = Player.first
another_first_player.update!(name: "いいいいい")
p first_player 
=>  #<Player id: 1, name: "あああああ"> # DB上のデータとは違う

another_first_player からupdateされたものはanother_first_playerには反映されますが、たとえ同じデータを参照していても、オブジェクトとして別のものである first_player にはその変更は反映されません。

これは絶対知っておいたほうがいいことです。
読み込んで表示するだけならば問題ないですが、今のデータをもとに更新を行う場合は、DBからデータを読み込んでから更新するまでの間に他で変更されないよう、きちんと排他制御をしましょう(後述します)。

ふたつめ: 変更がなければUPDATEは走らない

ActiveRecord:::Base は DBからデータをひいてきてメモリ上のオブジェクトが作られたあと、updateなりsaveなりを使ったとしても、変更がなければDB上のデータを更新しようとしません。

player = Player.first # #<Player id: 1, name: "あああああ">
player.update!(name: "あああああ")
# UPDATEは走らない

これで何が起こっているかはっきりしてきたかと思います。

def select_main_deck(deck)
  transaction do
    # 全てのデッキのselected_flgがfalseになる
    self.decks.each{ |deck| deck.update!(selected_flg: false) }
    # DB上ではselected_flgがfalseだが、メモリ上ではtrueのまま変わらないのでUPDATEされない
    deck.update!(selected_flg: true)
  end
end

こういったバグ、特に他人や昔の自分がかいたコードを読んだ場合かなり見つけにくいですね。

上に挙げた

  • 同じデータを違うオブジェクトから参照する
  • データの取得と更新を行う

の2つはバグの温床になりがちなので気をつけたいところです。

さて、この悲しい事態を避けるにはどうすればよかったのか考えてみましょう。

どうすればよかったのか

そもそも構造を変える

この場合なら、playersテーブルに selected_deck_id というカラムを追加して、そこで選択中のデッキを持っておいたほうが筋がいい気がします。
選択中のデッキを切り替えるときに複数のレコードを更新しなくて済み、データ不整合を起こすことがなくなります。

class Player < ApplicationRecord
  has_many :decks
  belongs_to :selected_deck, class_name: "Deck"

  def select_main_deck(deck)
    self.update!(selected_deck: deck)
  end
end

この場合に限らず、何らかの操作をしたとき、更新する部分があまり多くならないよう考えておくとバグが起こりにくいですね。

ロックをとる

どうしても上に挙げた方法が使えないときは、きちんと不整合を防ぐ仕組みが必要です。
例えばロックというDBからデータを読み込んでから更新するまでの間にデータが更新されないようにするDBの仕組みがあります。

たとえば所持金を増やしたいとき、知らないうちにDBのデータが更新されてしまうと困ったりしますよね。

class Player
  def add_money(val)
    # 今の所持金を200とすると
    added_money = self.money + 100
    # ここで、DB上でのplayerの所持金が300になる
    self.update!(money: added_money)
    # 本当は400なのに300に...
  end
end

「所持金を増やす」間に他のところでデータが書き換えられないようにしないといけません。
たとえば rails では、以下のように with_lock メソッドを使って書くと、with_lockに与えたブロックの中の処理が終わるまで別の場所でDB上のデータが更新できなくなります(こういった、一つのレコードに対しての更新を制御するものは行ロックと呼ばれています)。

class Player
  def add_money(val)
    with_lock do
      added_money = self.money + 100
      self.update!(money: added_money)
    end
  end
end

さらに気をつけたいのは、複数のレコードに対してロックを取る場合です。
一度ロックを取ってしまうと処理が終わるまでは他ではデータの更新ができないため、順番次第ではお互い更新が終わるまで待ち状態になる、いわゆ
るデッドロックという状態になってしまうことがあります。「食事する哲学者の問題」がわかりやすい例ですね。

デッドロックに陥らないためには、たとえば必ずidが大きい方からロックを取るなど、一意な順番でロックを取っていく必要があります。

class Player < ApplicationRecord
  has_many :decks
  belongs_to :selected_deck, class_name: "Deck"

  def pay_for(target_player, val)
    if self.id < target_player.id
      self.with_lock do
        target_player.with_lock do
           target_player.update!(money: target_player.money + val)
           self.update!(money: self - val)
         end
       end
     elsif self.id > target_player.id
      target_player.with_lock do
        self.with_lock do
           target_player.update!(money: target_player.money + val)
           self.update!(money: self - val)
        end
      end
    end
  end
end

まとめ

基本的にこういったデータの不整合が起きやすいのは、 同じDBのレコードを指すオブジェクトが複数できるとき です。

まずそういう状況に気づくこと、気づいたら意図しない場所での更新が起きないようにすること、そもそも広い範囲での更新が起きないよう考え直すことを意識していきたいですね。


  • 811 views
    • Tweet
    • このエントリーをはてなブックマークに追加

この記事を書いた人
くじら
新米エンジニアです。

「いいね!」するとDoRubyの最新記事を受け取ることができます。

Facebook

Twitterから最新記事を受け取るならこちら

Follow @doruby

Feedlyから最新記事を受け取るならこちら

follow us in feedly

おすすめの記事
  • 693 views
  • 2015-10-01
RailsのViewを自在にカスタマイズするための「Cosme」gem
  • 878 views
  • 2016-07-31
carrierwaveとfogでRiak CSへの画像アップロードを実装する

カテゴリ

Ruby/RailsRailsの小技Rubyの小技Gemの紹介ActiveRecord初心者向けWeb開発Java/ScalaテクUnixのあれこれデータベーススマホ開発HTML/CSS/JavaScriptデザイン製作インフラクラウドセキュリティエディタバージョン管理その他pythonWebマーケティングアクセス解析GoogleアナリティクスGoogleデータスタジオWeb広告SEOUI/UXソーシャルメディアアピリッツEC開発Webシステム開発コンサルティングWebデザインブロックチェーンゲーム紹介アプリ紹介ASP風景ライフハックパソコンツールガジェット仕事術健康生活書評Excel(エクセル)PowerPoint(パワーポイント)ゲーム制作/開発ゲームプランニングSpriteStudioマスターデータ入力/作成Unityキャラクターデザインゲームシナリオレベルデザインゲーム分析3DCGイラスト制作CG/アニメーション

    人気の記事
    最近の記事
    • 3,598 views
    • 2020-04-02
    Kali Linux 2020.2 導入と日本語化
    • 828 views
    • 2020-03-06
    rack-lineprofを改造して管理画面からファイル指定&ログ追跡出来るように
    • 802 views
    • 2020-03-05
    FactoryBot と Gimei を使って架空のユーザを作る
    • 584 views
    • 2020-02-04
    ActionCable実装しようwith webpack
    • 517 views
    • 2020-01-27
    この頃のプルリクに対するレビュー
    Facebook

      サイト情報
      • DoRubyとは
      • 株式会社アピリッツ

      ソーシャルアカウント
      • Facebook
      • Twitter

      企業情報
      • 会社概要
      • 採用情報
      • お問い合わせ
      サービス製品
      • レコメンドASP
      • サイト内検索ASP「Advantage Search」
      • オープンソースECサイト構築パッケージ「エレコマ」
      • 受注・在庫・商品情報一元管理「モールコネクター」
      • セキュリティ診断サービス
      • Googleアナリティクスセミナー

      Copyright © Appirits All Rights Reserved.