STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

MongoDB(Mongoid)を利用したRails開発のメリット

はじめに

はじめまして、hey でECとかレジのバックエンドエンジニアをやっている @ucks です。

STORES (以下、区別のため STORES EC と表記) と STORES レジでは、WebアプリケーションフレームワークRuby on Rails 、 データベースに MongoDB 、 ODM (RDB でいう ORM) に Mongoid を利用しています。

普段 RDB で開発しているエンジニアからすると MongoDB ってどうなの? と思う方も少なくないと思います。 そこで、今回は Rails で一般的なORMである Active Record を殆ど使ったことがない筆者が、 MongoDB + Mongoid での開発の良いところを RDB + Active Record と比較して紹介します。

以前 STORESってMongoDBを使ってるらしいけど正直どうなの? という記事もありましたが、今回はより具体的にコードを交えて紹介していこうかと思います。

スキーマ定義が不要

Active Record では、データを永続化する際、一般的にモデルとテーブルスキーマを別々に定義すると思います。 一方で、 Mongoid では、モデルの定義のみを行います。

Railsガイドの例を参考に Product というモデルで比較すると下記のようになります。

Active Record の場合

Active Record では、下記の様にモデルを定義し、

class Product < ApplicationRecord
end

下記の様に、テーブルスキーマを定義します。

CREATE TABLE products (
   id int(11) NOT NULL auto_increment,
   name varchar(255),
   PRIMARY KEY (id)
);

Mongoid の場合

Mongoid では、下記の様にモデル内に Field (RDBでいうColumn) を定義します。 id は、(データ型がBSON::ObjectID形式でよければ)勝手に生えるので書かなくても大丈夫です。

class Product
  include Mongoid::Document

  field :name, type: String
end

Mongoid の場合は、以上で定義は完了します。

Mongoid を利用すると、 Field (RDB でいう Column) をモデル内で定義でき、別途スキーマ定義が不要になります。 そのため、記述が少なくなるだけでなく、モデルを見ただけでデータの構造を把握しやすくなります。

マイグレーションが不要

MongoDB は、スキーマレスであるため、データ構造が変わっても、データベースのスキーマを更新する必要がありません。 そのため、データ構造を変更するプロジェクトのブランチを行き来しても、データベースのマイグレーションが不要です。

例えば、Product に価格 (Price) を追加するプロジェクトAと Product に説明文 (Description)を追加するプロジェクトBが並行して進んでいて、途中でブランチを行き来しても、何をすることもなく動きます。

プロジェクトAのブランチの Product モデル

class Product
  include Mongoid::Document

  field :name, type: String
  field :price, type: Integer
end

プロジェクトBのブランチの Product モデル

class Product
  include Mongoid::Document

  field :name, type: String
  field :description, type: String
end

ちなみに、消したフィールドのデータは自動で削除されたりはせず、残り続けるのでブランチを戻せば、以前入力したデータを利用することができます。

2つのブランチをマージした際には、(コードのコンフリクトは発生しますが…)両方のフィールド定義を取り込んであげれば、何事もなかったかの様に動きます。

class Product
  include Mongoid::Document

  field :name, type: String
  field :price, type: Integer
  field :description, type: String
end

また、新規のフィールドに初期値を与えたい時は、 default オプションを指定しておけば、フィールドが存在しない時に、引数の値で補完されます。 ただし、保存し直すまでは、 MongoDB 内に値が入らないので、検索する際は注意が必要です。

class Product
  include Mongoid::Document

  field :name, type: String
  field :price, type: Integer, default: 0
end

Mongoid では、この様にマイグレーション操作が不要なため、思い切ったデータ構造の変更を並行して進めることが容易になります。

モデルにちょっと追記して、仮実装を行い戻したくなれば、コードをリバートするだけで元に戻すことができます。(DB内には汚いデータが残りますが…)

Active Record に近い操作性

マッパーが切り替わると、インタフェース等を覚え直すだけでなく、適切な使い方等、様々な学習コストがかかるかと思います。 ですが、 Mongoid は Active Record に寄せられたインターフェースなので、 Active Record に慣れている人は、非常に少ない学習コストで利用することができると思います。

例えば、作成/更新/削除は次のように行えます。

product = Product.new(name: '製品0001', price: 100)
product.save!
product.update!(price: 200)
product.destroy!

#where#find_by 等の条件検索も完全一致の場合、 Active Record とほぼ同様に利用することができます。 ただし、部分一致や範囲指定の場合は、少しだけ書き方が変わります。

例えば、 name製品 から始まり、 price100 より大きくて、 200 未満の Product を検索したい場合は、下記のように記述できます。(個人的には Active Record の記述よりも好み。)

Product.where(name: /\A製品/).gt(price: 100).lte(price: 200)

この様に、 Mongoid を利用すると、普段から Active Record を利用しているエンジニアであれば、非常に低い学習コストで MongoDB を利用することができます。

オブジェクトや配列を保存できる

MongoDB は Document という単位でデータの保存を行います。 Document とは、 RDB の Record の様なもので、BSON形式(JSONを機械が読みやすい様にバイナリで書き換えた形式)のデータです。 BSONは、オブジェクトや配列をネストすることができるため、 MongoDB では1つの Document で1:Nの情報を扱うことができます。

Mongoid では、 embeds_one (1:1) 、 embeds_many (1:N) でネストした構造を定義することができます。

embeds_many (1:N)

embeds_many は 1:N のネスト構造を表します。 JSONvalue が配列で、その中身にオブジェクトが入っている様な状態を定義している感じです。

例えば、 Product は定期的に改訂がはいり、その情報を同じ Document に格納しておきたい場合は、下記の様になります。

まず、改訂情報を保存する Revision モデルを定義します。

class Revision
  include Mongoid::Document

  embedded_in :product

  field :number, type: String
end

次に、 Product モデルに Revision が複数埋め込まれることを定義します。

class Product
  include Mongoid::Document

  field :name, type: String

  embeds_many :revisions
end

これで、下記の様な Product に複数の Revision を持ったデータを保存することができます。

{
  "name": "製品A",
  "revisions": [
    {
      "number": "0001"
    },
    {
      "number": "0002"
    }
  ]
}

また、配列でネストされた情報で検索したい場合は #elem_match を利用することで検索できます。

Product.elem_match(revisions: { number: /\A0001/ }) # この位簡単な検索であれば Product.where('revisions.number': /\A0001/) でも良い

MongoDB では、 1ドキュメント16MBという制限があるため、あまり大量の1:Nのデータを保存するのには適しませんが、少量の1:Nのデータで、アクセス頻度が高い場合、1クエリでまとめて fetch することができるメリットがあります。 (1ドキュメントが大きくなりすぎると1クエリのトラフィックも増えるので注意が必要です。)

embeds_one (1:1)

embeds_one は 1:1 のネスト構造を表します。 JSONvalue に直接オブジェクトが入っているような構造を定義している感じです。 1:1 の表現なので、ネストさせずに、同じ階層にフィールドを定義することもできますが、 embeds_one を使うと関心事をネストされた1つのモデルにまとめることができます。

例えば、 Product は、 widthheightdepth といったサイズ情報を持ち、全てに値が入るか、全てが空のどちらかしか許容されないモデルを考えます。

ネストさせずに、同じ階層にフィールドを定義する場合は下記の様になります。

class Product
  include Mongoid::Document

  field :name, type: String

  field :width, type: Integer
  field :height, type: Integer
  field :depth, type: Integer

  validates :width, presence: true, if: :has_size?
  validates :height, presence: true, if: :has_size?
  validates :depth, presence: true, if: :has_size?

  def has_size?
    width? || height? || depth?
  end
end

次に embeds_one を利用した構造を定義すると下記の様になります。

class Product
  include Mongoid::Document

  field :name, type: String

  embeds_one :size
end
class Size
  include Mongoid::Document

  embedded_in :product

  field :width, type: Integer
  field :height, type: Integer
  field :depth, type: Integer

  validates :width, presence: true
  validates :height, presence: true
  validates :depth, presence: true
end

embeds_one を利用すると、サイズに関する情報が Size モデルにまとまり、バリデーションの条件が簡単になっていることがわかると思います。

RDB では、 has_one で似た様な構造は定義することができますが、リレーション先に必ずデータが存在することをスキーマで保証するには工夫が必要です。(遅延制約で相互参照させる等) 一方、 Mongoid はネストされた1:1構造も、1つの Document でまとめて書き込むことができるので、書き込むタイミングで検証すれば、必ずデータが存在すること保証することができます。

例えば、 Product にサイズ情報が必須な場合は、下記の様になります。

class Product
  include Mongoid::Document

  field :name, type: String

  embeds_one :size

  validates :size, presence: true
end

昨今は、 RDB にもJSON型の採用が増えつつありますが、 MongoDB の方が記述の簡潔さや複雑な検索の行いやすさには分があると思います。

おわりに

今回は、 STORES EC とレジで利用している MongoDB 、 Mongoid の良いところについて紹介しました。

正直、 MongoDB を利用して開発していると、トランザクションの利用には制限があったり、外部キー制約等がないこともあり、整合性が求められるプロダクトには向いていないと思うことも多々あります。 そのため、もし何かの機会に EC を一から作ることになっても、個人的には、 MongoDB を推すことはないかもしれません。

ただ、 STORES EC とレジでは、 RDB への乗り換えを検討した結果として MongoDB を活かす選択をしており、 MongoDB を触っていく必要があるので、 MongoDB の向いている場面や使い方についての知見を得ることができます。 また、データ構造を変えやすいというメリットは、変化の速いプロダクトや多人数での並行した開発に非常に向いており、 STORES EC での開発にマッチしているのではと思う場面も少なくありません。

この記事を通して、 MongoDB や Mongoid 、 STORES に興味を持っていただける方が増えれば幸いです。