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

隠れた宝石:ActiveRecordのstoreとstore_accessor徹底解説

Railsは、さまざまな場面で役立つ便利な組み込みツールを多数備えた大規模フレームワークです。このシリーズでは、Railsの巨大なコードベースの中に隠れている、あまり知られていないツールを紹介していきます。

本記事では、ActiveRecordが提供するstoreメソッドとstore_accessorメソッドに焦点を当てます。どちらも、JSONやYAMLといった構造化データをデータベースの1つのカラムに格納するユースケースを想定したものです。store_accessorは、getterメソッドでモデルを汚すことなくこれらのデータへアクセスできる便利な手段を提供し、storeはさらに一歩進んで、選択したフォーマットへのシリアライズ/デシリアライズを透過的に行ってくれます。この機能がどこで役立つのかを理解するために、リレーショナルデータベースにJSONを保存する方法と、その理由についても見ていきましょう。

データベースにJSONを保存する

はじめに断っておきますが、本記事で「データベース」と言う場合はリレーショナルデータベース、特にRailsコミュニティで最も広く使われているPostgreSQLとMySQLを指します。

「なぜリレーショナルデータベースにJSONを保存したいのか?」と疑問に思うかもしれません。確かに、リレーショナルデータベースの恩恵を最大限に引き出すには、データを分割してリレーションシップ(関係)をデータベース側で強制したり(外部キーなど)、インデックスを作成してクエリ性能を向上させたりすることが重要です。

リレーショナルデータベースモデルの弱点のひとつは、データ構造を事前に把握しておく必要があり、かつテーブル内のすべての行で同一でなければならない点です。アプリケーションがこうした要件を満たさないデータを中心に構築されているなら、NoSQLデータベースを検討する価値があるでしょう。しかし、ほとんどのWebアプリでは、慣れ親しんだリレーショナルデータベースを使い続けつつ、動的なデータ構造を賢く「さりげなく」混ぜ込みたいはずです。そうしたケースでは、JSONカラムのような仕組みが非常に理にかなっています。

jsonとjsonbの違い

PostgreSQLには2種類のJSONカラムがあります:jsonjsonbです。主な違いは、jsonbは書き込み時にパースされる点です。つまり、データベースが高速にクエリできる形式でデータが保存されます。その代わり、JSONがすでにパース済みであるため、テキストとして出力すると、ユーザーが入力した内容と完全には一致しない場合があります。たとえば、重複キーが削除されたり、キーの順序が元のものと異なったりする可能性があります。

PostgreSQLの公式ドキュメントでは、特別な理由がない限り、ほとんどの場合はjsonbを使うべきだとされています。

MySQLのjsonカラムは、PostgreSQLのjsonbとほぼ同様に動作します。「ユーザーが入力した通りの内容」を出力したい場合は、varcharカラムなどを使うしかないでしょう。

JSONカラムとテキストカラム

データを事前にパースできるようになるだけでなく、同じデータをテキストフィールドに保存する代わりにJSONカラムを使うことで、データそのものを使ったクエリが可能になります。たとえば、特定のキーバリューペアを持つすべてのレコードを検索できます。ただし、Rails自体はJSON固有のクエリをあまりサポートしていない点に注意してください。これらはデータベース固有の機能だからです。したがって、これらの機能を活用したい場合は、SQLクエリを直接記述する必要があります。

RailsでのJSONカラム

Railsはマイグレーションでjson(PostgreSQLではjsonb)カラムを作成できます:

class CreateItems < ActiveRecord::Migration[7.0]
  def change
    create_table :items do |t|
      t.jsonb :user_attributes

    ...
    end
  end
end

このカラムを読み込むと、Hashが返されます:

> Item.first.user_attributes
  Item Load (0.6ms)  SELECT "items".* FROM "items" ORDER BY "items"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> {"color"=>"text-red-400"}
> Item.first.update!(user_attributes: {color: "text-blue-400"})
> Item.first.user_attributes.dig(:color)
=> "text-blue-400"

Hash型の属性が手に入ったので、値を読み書きするヘルパーメソッドをモデルに追加したくなるかもしれません:

class Item < ApplicationRecord
  def color=(value)
    self.user_attributes["color"] = value
  end

  def color
    user_attributes.dig("color")
  end
end

このようなメソッドは問題なく機能しますが、扱うJSONキーが多くなるとすぐに手に負えなくなります。幸い、Railsがしっかりとサポートしてくれます。

ActiveRecordのstoreとstore_accessor

データベースにJSONを保存する際には、「シリアライズ」と「アクセス」という2つの側面があります。json型のカラムを使っているなら、シリアライズについては心配いりません。Railsとデータベースアダプタが処理してくれるため、store_accessorのセクションまで読み飛ばしても構いません。一方、テキストカラムにデータを保存している場合は、ActiveRecordのstoreメソッドの出番です。このメソッドは、カラムに書き込むデータが選択したフォーマットでシリアライズされることを保証します。

ActiveRecordのstore

ActiveRecordには、カラムへの読み書き時に自動的にシリアライズを行うstoreメソッドがあります:

class Item < ApplicationRecord
  store :user_attributes, accessors: [:color], coder: JSON
end

ここで:user_attributesは使用するカラム、accessorsはアクセスしたいキーのリスト(この例ではcolorのみ)、最後にデータのエンコード方法を指定します。ここではJSONを使っていますが、YAMLや独自のエンコーディングなど、好きなものを使用できます。このメソッドは、指定したcoderによるシリアライズを処理し、内部でstore_accessorを呼び出します。

ActiveRecordのstore_accessor

モデルにゲッター/セッターメソッドを作成するにはstore_accessorを使います:

class Item < ApplicationRecord
  store_accessor :user_attributes, :color
  store_accessor :user_attributes, :name, prefix: true
  store_accessor :user_attributes, :location, prefix: 'primary'
end

ここでもuser_attributesは使用するデータベースカラムで、その後にJSONデータ内で使いたいキーを指定します。さらに、プレフィックス(またはサフィックス)を使うオプションもあります。なお、store_accessorはネストしたデータをサポートせず、トップレベルのキーバリューペアのみ対応しています。prefixsuffixオプションには、真偽値、文字列、シンボルのいずれかを渡せます。真偽値trueを渡すと、カラム名がプレフィックス/サフィックスとして使われます。

item = Item.create!(color: 'red', user_attributes_name: 'Jonathan', primary_location: 'New Zealand')
#<Item:0x000055d63f4f0360
 id: 4,
 user_attributes: {"color"=>"red", "name"=>"Jonathan", "location"=>"New Zealand"}>
item.color
"red"
item.user_attributes_name
"Jonathan"
item.name
NoMethodError: undefined method `name'...
item.primary_location
"New Zealand"

実務での活用例

筆者自身、事前にスキーマが分かっている典型的なリレーショナルデータベース設計から外れる必要があったのは、ごくまれなことでした。しかし、その数回の経験において、これらの機能のおかげでデータベース構造がよりクリーンでシンプルになりました。

筆者が遭遇した例のひとつは、ユーザーが自分のアカウントを接続する複数のAPIをサポートするケースです。APIによって認証方式が異なるため、これは厄介な問題になります。ユーザー名+パスワードを使うものもあれば、APIキーだけのもの、さらにはAPIキー+シークレット+マーチャントIDが必要なものもあります。ひとつの方法は、テーブルにカラムを追加し続けることですが、その多くはほとんどのプロバイダーに対してnullになってしまいます。しかしjsonカラムを使えば、特定のAPIが必要とする値だけを保存できます。

また、筆者が開発中のサイドプロジェクトでも、アイテムに対してユーザー定義の属性を含む任意の属性を設定できるようにするために、JSONストレージを利用しています。流動的で予測不可能な性質を持つこの種のデータには、既知の属性に対するstore_accessorと組み合わせたJSONストレージが自然な選択となります。

まとめ

JSONデータ(およびそれを扱うActiveRecordのヘルパー)は、データやデータ構造が変わりやすい、あるいは事前に分からない場合に非常に有用です。もちろん、このようなデータ保存は他の多くの技術選択と同様にトレードオフです。個々のレコードに対してデータ構造の柔軟性を大きく得られる一方で、データベース制約がもたらすデータ整合性の一部を犠牲にすることになります。また、通常のActiveRecordのクエリやjoinsなどを使ったレコード横断的な検索能力も低下します。

以下にいくつかの経験則を示します。次のような場合は:

  1. すべての行でJSONキーが同一であることが分かっている、または
  2. 他のデータベーステーブルのID(主キー)を保存している、または
  3. テーブルからレコードを検索するために使う値をJSON内に保存している

そのような場合は、データベースにデータ整合性の強制を任せられる新しいテーブルを作成する方が良いでしょう。一方、他のテーブルと直接関連しない行固有のデータを保存しているのであれば、JSONがデータベース構造の簡素化に役立つかもしれません。

  1. JSONをExcelに変換する方法!Power Queryとオンラインツールの使い方を徹底解説

    JSON(JavaScript Object Notation)は、さまざまな種類のデータを保存するために広く利用されているデータ形式です。データの保存だけでなく、2つのサーバー間でデータをやり取りする際にも活用されており、APIなどを通じてWebサーバーと通信する場面でよく目にします。JSONファイルの構造は非常にシンプルで、配列とオブジェクトで構成されています。典型的なJSONファイルでは、キーと値のペアが配列内の各オブジェクトに格納されており、インデックスとキーを指定することで目的のデータにアクセスできます。プログラマーや開発者にとってJSONの構造はごく基本的なものですが、一般ユーザー

  2. Google Playが選ぶ2022年「ヒドゥン ジェム」部門ベストアプリTOP5

    Google Playストアには、ゲーム、ソーシャル、スマホの最適化、バッテリー節約など、あらゆるカテゴリで数百万ものアプリが登録されています。その中には、有名アプリほど注目されていないものの、実際に使ってみると非常に便利で「これなしでは困る」と感じられるアプリが数多く存在します。Google Playは毎年、こうした「隠れた名作=ヒドゥン ジェム」カテゴリで上位にランクインしたアプリを発表しています。今回は、2022年に同部門で受賞したAndroidアプリの中から、特におすすめの5本をご紹介します。Socratic – Math Answers & Homework HelpSocr