こんにちは!BOOTH部所属エンジニアのRND(らんど)です。
2023年9月29日に開催されたPIXIV MEETUP 2023のライトニングトークセッションにて「限界ORM!BOOTHとギフトとライブラリ」というタイトルで発表を行いました。
本記事はそちらの発表内容を元に記事に起こしたものです。
はじめに
BOOTHは2013年にリリースされ2023年の12月に10周年を迎えるサービスで、Ruby on Railsで開発されています。
RailsではORMとしてActiveRecordが付属していますが、プロダクトを開発する中でActiveRecordの標準の機能では対応できないクエリが必要となるケースが多々あります。
そういったケースにBOOTHではどのように立ち向かっているのか、BOOTHのギフト機能の開発でぶつかった2つの事例を紹介します。
BOOTHのギフト機能の概要について
ギフト機能とは、BOOTH上で販売されているダウンロード商品を購入する際にギフトとして購入することができる機能です。
ギフトとして購入した商品には、専用のギフト用URLが発行されます。
このURLは商品の購入者でなくともアクセスでき、アクセスしたページで商品を受け取ることができます。
受け取った商品はBOOTH上のライブラリなどからファイルを閲覧・DLできるようになります。
テーブルの構造は下図のようになっており、ギフトの場合と通常の購入の場合とで商品の所有者が異なる仕様になっています。この仕様のために後述するBOOTHのライブラリ画面は大きく変更が必要になりました。
事例1:ライブラリでの表示
BOOTHのライブラリ画面は自分が所有しているダウンロード商品の一覧を表示する画面です。 ギフト機能導入以前は、自分が購入したダウンロード商品を入手順に並べているだけの画面でした。 ギフト機能によって、購入者と所有者が一致しなくなったため、このロジックは変更が必要になりました。 ギフトの仕様上、ギフトとして購入した場合とそれ以外の場合では所有している商品を取得するクエリが異なります。 これまで通り自分が所有している商品を入手順に並べる場合、ギフトで受け取った商品とそれ以外の商品を合わせてから入手順に並び替える必要があります。
こういったケースではSQLのUNION句を用いて、異なる経路でJOINしたテーブルをひとつにまとめる必要がありますが、ActiveRecordにはUNION句を発行する機能がありません。 gemを用いてActiveRecordの機能を拡張するなどの方法もありますが、BOOTHでは実装方法について検討する前に要件の見直しを検討しました。
その結果、ライブラリのページをギフトとして入手した商品と購入した商品とで分割することにしました。
ライブラリ画面をギフトとそれ以外で分割する形であっても、ユーザー体験に支障がないと判断したためです。
これによって2つの経路でJOINされたテーブルをまとめる必要がなくなりました。
事例2:ギフトの複数回受け取り
ライブラリ画面をギフトに対応したものに改修する上でもう一つ課題がありました。 BOOTHのギフト機能には、同一商品のギフトを複数回受け取れるという仕様があります。 チームで議論し、複数回ギフトとして受け取った商品については直近に受け取ったものだけをライブラリに反映するのが良いだろうという結論になりました。
このとき、GROUP BYで商品ごとにまとめ、ORDER BYでギフトの受け取り日時で並べ替えるといったクエリが必要になります。 SQLではORDER BYがGROUP BYよりも後に実行されるという仕様のため、これを実現するには集約関数を用いたクエリか、サブクエリを用いたクエリが必要です。
集約関数を用いたクエリについてはActiveRecordの標準の機能では記述できないため、SQLの一部を直接書く必要があります。
サブクエリを用いた方法はActiveRecordのfromを使うことでActiveRecordの標準機能の範囲内で記述できます。ただし、このクエリはギフトの件数が多くなってきた場合にパフォーマンスが落ちます。
さらに根本的な解決として、ライブラリ専用のテーブルを作成し、ギフトや購入の際にテーブルを更新する設計にする案が考えられます。
この設計であれば前述のような複雑なクエリが不要になります。
それぞれの案のメリットデメリットを考えると
集約関数やサブクエリを用いた案
- SQLを直接書く、または複雑なActiveRecordの操作が必要 △
- ライブラリ画面の将来的な仕様変更・機能追加に弱い △
- パフォーマンス △
- 実装コストが低い ◯
テーブル設計を変える案
- 複雑なクエリを発行する必要がない ◯
- ライブラリ画面の将来的な仕様変更・機能追加に強い ◯
- パフォーマンス ◯
- 実装コストが高い △
将来的にはライブラリ用にテーブル設計を変更した方がメンテナンスしやすく変更が容易で様々な要件に対応できます。しかし実装コストが高く、まずはユーザーにとって必要なものを素早く出すことを優先し、サブクエリを用いた方法を採用することにしました。
まとめ
Railsのアプリケーションを書いている中で、ActiveRecordが対応していない複雑なクエリが必要になるケースは頻繁に生じます。そういったケースが生じる背景にはいくつか種類があります。
- ビジネスロジックが複雑になっている
- テーブルやモデルの設計がビジネスロジックとうまく合致していない
- 標準SQLにない機能を使用している
今回紹介した事例1のように、ビジネスロジックが必要以上に複雑になっている可能性を考え、まずは要件と仕様を見直し、ビジネスロジックの複雑化を回避できないかを考える必要があります。
要件を見直した結果、ユーザー体験のために必要な仕様であれば、今回紹介した事例2のようにテーブル設計から見直すことでシンプルにビジネスロジックを表現できる形がないかを探るべきでしょう。
また、MySQLのインデックスヒントを使用したいケースなど標準SQLにない機能を使用する場合にはActiveRecordの標準の機能では対応できません。こういったケースでは特定のDBにロックインするデメリットを受け入れた上でActiveRecordを拡張してそれらの機能に対応する選択肢が考えられます。
このように、ActiveRecordが対応していないクエリが必要になったタイミングで要件や設計の見直しを行うのが良いと思います。一方で、理想の形を実現するのにコストがかかる場合には、まずはより素早く実装できる選択肢をとるべきです。 新しい機能の使われ方には不確実な部分も大きく、機能を出してみてから見直すことでより適切な設計が見えてくる場合もあります。
おわりに
ピクシブではバックエンドエンジニアを募集中です。興味をお持ちの方は下記URLよりエントリーをお待ちしております。 hrmos.co