Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

肥大化するusersテーブルを飼いならす方法

クライアントのアプリはどうやってこの状態に陥ったのか?

詳細に入る前に、アプリがどのようにこの状態に至るのかを理解してみましょう。まずはシンプルなusersテーブルから始めます。数週間後、「最終ログイン時刻」を判定できるようにする必要が生じ、users.last_sign_in_atを追加します。次にユーザーの氏名が必要になり、first_namelast_nameを追加します。Twitterハンドル?もう1カラム追加。GitHubプロフィール?電話番号?こうして数ヶ月もすると、テーブルは想像を絶する姿になります。

何が問題なのか?

これほど巨大なテーブルには、いくつかの問題が潜んでいます。

  1. Userモデルが互いに無関係な複数の責務を抱えることになり、理解・変更・テストが難しくなる。
  2. アプリとデータベース間のデータ交換に余分な帯域が必要になる。
  3. 巨大なモデルを保持するために、より多くのメモリを消費する。

このアプリでは、認証・認可の目的で毎回のリクエストごとにUserを取得していましたが、実際に使われていたのはごく一部のカラムだけでした。この問題を解決すれば、設計とパフォーマンスの両方が改善します。

テーブルを抽出する

この問題は「使用頻度の低いカラムを新しいテーブル(または複数のテーブル)へ抽出する」ことで解決できます。例えば、プロフィール情報(first_nameなど)をprofilesテーブルに抽出するには、以下の手順を実行します。

  1. users内のプロフィール関連カラムを複製するカラムを持つprofilesテーブルを作成します。
  2. usersprofile_idを追加します。今のところNULLのままにしておきます。
  3. usersの各行に対して、プロフィール関連カラムを複製した行をprofilesに挿入します。
  4. usersの対応する行のprofile_idを、手順3で挿入した行に向けます。
  5. users.profile_idを非NULL制約にしてはいけません。アプリはまだその存在を認識していないため、壊れてしまいます。

続いて、users.first_nameへの参照をprofiles.first_nameなどに置き換えていく必要があります。抽出するカラムが少数で、参照箇所も少なければ手作業での置き換えをお勧めします。しかし、「うわっ、最悪の作業だ!」と思い始めたら、別の方法を探すべきです。

問題を放置しないでください。誰もが避けて通るコードはさらに劣化し、ますます手を付けられなくなります。悪循環を断ち切る最も簡単な方法は、小さく始めることです。

クライアントがこの問題をどう解決したのか気になる方は、このまま読み進めてください。

一行ずつコードを直していく

最も漸進的なアプローチは、古いカラムへの参照を一度に1つずつ修正することです。ここでは、first_nameusersからprofilesへ移動させることに焦点を当てましょう。

まず、次のコマンドでProfileモデルを作成します。

rails generate model Profile first_name:string

次に、usersからprofilesへの参照を追加し、users.first_nameのデータをprofilesへコピーします。

class ExtractUsersFirstNameToProfiles < ActiveRecord::Migration
  # 本番コードへの依存を断ち切るため、モデルを再定義します。
  # コールバックなどを含まない素のモデルが必要です。また、将来的に
  # モデルを削除するとマイグレーションが壊れる可能性もあります。
  class User < ActiveRecord::Base; end
  class Profile < ActiveRecord::Base; end
 
  def up
    add_reference :users, :profile, index: true, unique: true, foreign_key: true
 
    User.find_each do |user|
      profile = Profile.create!(first_name: user.first_name)
      user.update!(profile_id: profile.id)
    end
 
    change_column_null :users, :profile_id, false
  end
 
  def down
    remove_reference :users, :profile
  end
end

各ユーザーが必ず1つのプロフィールを持つことを強制できるため、usersからprofilesへの参照は、逆方向の参照よりも望ましいと言えます。

データベース構造が整ったら、UserからProfilefirst_nameを委譲できます。クライアントにはいくつかの要件がありました。

  1. アクセサは関連付けられたProfileを使用すること。また、非推奨アクセサがどこから呼ばれたかをログに出力すること。
  2. 非推奨アクセサを使っている既存コードを壊さないよう、Userの保存時に自動的にProfileも保存されること。
  3. User#first_name_changed?などのActiveModel::Dirty系メソッドが引き続き正常に動作すること。

つまり、Userは次のようになります。

class User < ActiveRecord::Base
  # 呼び出し側のコードがProfile#first_nameを知らず、
  # 引き続きUser#first_nameを参照している可能性があるため、autosaveが必要です。
  belongs_to :profile, autosave: true
 
  def first_name
    log_backtrace(:first_name)
    profile.first_name
  end
 
  def first_name=(new_first_name)
    log_backtrace(:first_name)
 
    # superを呼ぶことで、User#first_name_changed?などのメソッドが
    # 期待通りに動作し続けます。
    super
 
    profile.first_name = new_first_name
  end
 
  private
 
  def log_backtrace(name)
    filtered_backtrace = caller.select do |item|
      item.start_with?(Rails.root.to_s)
    end
    Rails.logger.warn(<<-END)
A reference to an obsolete attribute #{name} at:
#{filtered_backtrace.join("\n")}
END
  end
end

これらの変更後もアプリは従来どおり動作しますが、Profileへの余分な参照によって多少遅くなる可能性があります(パフォーマンスが問題になったら、AppSignalのようなツールを使いましょう)。このコードはレガシー属性へのすべての参照をログに記録します。grepでは見つけられない参照(例:user[attr] = ...user.send("#{attr}=", ...))も記録されるため、grepが役に立たないケースでも、すべての参照箇所を特定できます。

この仕組みが整えば、「毎朝1件のusers.first_name参照を修正する」(一日の始まりに小さな成功体験を得るため)や「昼頃に1件修正する」(集中した午前中のあとに軽めの作業をするため)といった具合に、定期的なペースで修正に取り組めます。この継続的なコミットメントこそが重要です。目標は、問題修正への心理的なハードルを下げることだからです。上記のコードを置いたまま何もしなければ、アプリはさらに貧弱になってしまいます。

すべての非推奨参照を削除し(grepとログで確認)、最後にusers.first_nameカラムを削除します。

class RemoveUsersFirstName < ActiveRecord::Migration
  def change
    remove_column :users, :first_name, :string
  end
end

Userに追加したコードも、もはや不要になったので忘れずに取り除きましょう。

制限事項

この手法があなたのケースにも当てはまるかもしれませんが、いくつかの制限がある点に留意してください。

  • User.update_allのような一括更新クエリには対応していません。
  • 生SQLクエリには対応していません。
  • モンキーパッチを壊す可能性があります(依存関係がモンキーパッチを導入している場合もある点に注意してください)。
  • profiles.first_nameだけが更新されusers.first_nameが更新されないままの場合、UserProfileのデータが不整合になる可能性があります。

一部の制限は工夫で克服できます。例えば、サービスオブジェクトやProfileのコールバックを使って両モデルの同期を保つ方法があります。PostgreSQLを使用しているなら、移行期間中はマテリアライズドビューの活用も検討するとよいでしょう。

まとめ

この記事の最も重要な教訓は、「コードの不吉な臭いから目を背けず、正面から向き合うこと」です。タスクがあまりに大きく感じられるなら、定期的なペースで反復的に取り組みましょう。この記事では、テーブルの抽出が難しい場合に検討できるひとつの手法を紹介しました。自社の状況に適用できない場合は、別の方法を探してください。やり方がわからなければ、筆者までご連絡いただければお手伝いします。大切なコードを腐らせてしまわないようにしましょう。

  1. Windows 10を高速化する7つの対処法|動作が重い・遅いと感じたら試したい最適化ガイド

    Windows 10パソコンの動作が遅くてお困りの方は、この記事が参考になるはずです。これまでに、新品同様のはずなのに動作が重いPCを含め、多くのWindows 10マシンの低速化トラブルを目にし、実際に解決してきました。経験上、Windowsの動作が遅くなる原因はいくつかの要因に分けられます。Windows(すべてのバージョン共通)の動作が遅くなる主な原因は以下の通りです。マルウェアへの感染Windows起動時に自動で立ち上がるプログラムが多すぎるページファイル(仮想メモリ)の設定が不適切ハードディスクの故障・劣化本記事では、Windows 10 PCを快適に動作させるための具体的な手順を順

  2. Facebookはどうやってユーザーの信頼を取り戻すのか?カンブリッジ・アナリティカ事件後の対策を徹底解説

    Facebookとマーク・ザッカーバーグCEOは、最近の騒動によってメディアの話題を集めています。すべてはCambridge Analytica(カンブリッジ・アナリティカ)によるデータプライバシー問題から始まり、その後、ザッカーバーグ氏の辞任が噂される事態にまで発展しました。「退任後はスキンケアブランドを支援する」という噂も流れましたが、これは結局、エイプリルフールの冗談だったことが判明しています。 しかし、本当に問われているのはザッカーバーグ氏の今後ではなく、ソーシャルメディア大手であるFacebook社が、失われたユーザーの信頼をいかに回復するかという点です。この件については様々な憶測や