MongoDB

2011年12月13日 (火)

Mongorillaでリレーション管理

Mongorillaでリレーションを管理する例として、読書管理アプリを考えてみました。
読書管理アプリは、本棚に本を登録し、どの本をどこまで読んだかを管理するツールです。
本を読む前に、本棚から本を取り出して、本を読んだ後に読んだページ数を登録することで更新します。
モデル図は下記の通り。
Bookshelf

  • UserとBookshelfは1対1の外部リレーション関係。
  • UserとReadingLogは1対多の外部リレーション関係。
  • UserとCurrentBookInfoは1対1の内部リレーション関係。
  • BookshelfとBookInfoは1対多の内部リレーション関係(Array形式)。

処理

本追加時

Bookshelfにbook_info_recordsにrecordを追加する本の数だけ追加。
book_info_recordsは、 Bookshelfのembedのため、複数の本もAtomicに同時に追加できる。

読む本を選択時

1. current_book_info_recの内容を、Bookshelfのbook_info_recordsのうち同じbook_idのレコードのデータに上書きする。
2. Bookshelfのbook_info_recordsのうちユーザが選択したbook_idのレコードで、 current_book_info_recの内容を上書きする。
2.の処理が失敗しても、1.の処理を何度実行しても同じ結果のため、問題がない。

読書情報記述時

1. ReadingLogに読んだユーザと本を読んだページ数を登録する。  
2. Userのtotal_read_page_numとcurrent_book_info_recのpage_num,updated_atを更新する。  
これらは1レコードのためatomicに更新できる。
また、2が失敗しても1.の情報や過去のReadingLog を元にデータを補正できる。

注意点

current_book_info_recの本のレコードは、bookshelfのbook_info_recordsの同じ本のデータと差異があります。
読む本を変更した際に、book_info_recordsにcurrent_book_info_recのデータを反映することで更新されます。
current_book_info_recを作成せず、直接bookshelfのbook_info_recordsを更新すればいいと感じる人も 多いと思いますが、あえてcurrent_book_info_recと分けている理由は、ユーザの一動作につき、できるだけ少ない documentの読み込みと更新を実現するためです。
もし、bookshelfのbook_info_recordsを直接更新しようとすると、読書後にUserのtotal_read_page_num の更新とBookshelfのbook_info_recordsの更新という2回のdocumentの更新が必要となります。
トランザクション のないMongoDBでは、片方のdocumentしか更新されなかい不整合の状態が発生し得ます。
そのため、不整合が起きないように単一のdocumentのみの更新にするか、起きてもリカバリーできるような documentの更新順を意識して、アプリケーションを作成する必要があります。

実装

github上のソース

user.rb

#coding: utf-8
require 'mongorilla'
require File.expand_path("./current_book_info", File.dirname(__FILE__))
require File.expand_path("./bookshelf", File.dirname(__FILE__))
require File.expand_path("./reading_log", File.dirname(__FILE__))
class User
  UserFields = [:_id,:name,:total_read_page_num,:current_book_info_rec]
  include Mongorilla::Document

  def self.build(name)
    User.create( :name => name,
              :total_read_page_num => 0,
              :current_book_info_rec => {}
              )
  end

  def current_book_info
    @current_book_info ||= CurrentBookInfo.new(self,self.current_book_info_rec)
  end

  def current_reading_logs
    ReadingLog.find({:user_id => self.id},{:sort => [["created_at","desc"]],:limit=>5})
  end

  def bookshelf
    return @bookshelf  if @bookshelf
    @bookshelf = Bookshelf.find_one("user_id" => self.id)
    unless @bookshelf
      @bookshelf = Bookshelf.build(self.id)
    end
    return @bookshelf
  end
end

self.build

デフォルト値をセットするために、呼び出し元にcreateではなく、buildを呼んでもらうようにする

current_book_info

current_book_infoはcurrent_book_info_recのHashを使ってModelを作成し、Userと1対1の内部リレーションを形成。 Userの使用元には、current_book_info_recのフォーマットを意識させない。

current_reading_logs

ReadingLogはUserと1対多の外部リレーション。 すべて取ってくると大きすぎるので、直近5件のみを取得

bookshelf

BookshelfはUserと1対1の外部リレーション。 Bookshelfのデータは大きくなることが予測されるので、外出しすることでUserのfindの処理を軽減させる。 まだ該当ユーザのBookshelfが作成されていない場合は、参照時に作成することで、Userのレコードしかない 状態では、必ずBookshelfが作成されるため、整合性がとれた状態になるようになる。

book_info_field.rb

#coding: utf-8
module BookInfoFields
  Fields = [:book_id,:page_num,:created_at,:updated_at]

  Fields.each do |f|
    define_method(f) { @info[f.to_s] }
  end

  def renewal(new_info)
    Fields.each do |f|
      @info[f.to_s] = new_info.send(f)
    end
  end
end

renewal

current_book_info_recとbook_info_recordsの各レコードは同じフィールドを持つので、共通の処理をモジュールとして作成していて、renewalは、current_book_info_recのレコードをbook_info_recordsの該当レコードに反映する時、またはbook_info_recordsのレコードをcurrent_book_info_recに反映する際に使用。

current_book_info.rb

#coding: utf-8
require File.expand_path("./book_info_fields", File.dirname(__FILE__))
class CurrentBookInfo
  include BookInfoFields

  def change(new_info)
    @user.changes["$set"] ||= {}
    renewal(new_info)
    @user.changes["$set"]["current_book_info_rec"] = @info
  end

  def inc(num)
    now = Time.now
    ReadingLog.create(:book_id=>self.book_id,:user_id=>@user.id,:page_num => num,:created_at => now)
    @user.changes["$inc"] ||= {}
    @user.changes["$inc"]["current_book_info_rec.page_num"] = num
    @user.changes["$set"] ||= {}
    @user.changes["$set"]["current_book_info_rec.updated_at"] = now
    @info["page_num"] += num
    @info["updated_at"] = now
  end

  def initialize(user,rec)
    @user = user
    @info = rec
  end
end

change

current_info_recの内容をnew_infoに更新し、save時にDBに反映されるようにしている。HashやArrayは要素の変更をMongorillaが認識しないため、Mongoドキュメントのオブジェクトのchangeを自分で編集している。@user.current_book_info_rec = @info.dupにすればMongorillaが変更を認識するため、代替可能。

inc

読書した情報をReadingLogに書き込み、current_book_info_recを更新して、更新した内容をsave時にDBに反映されるようにしている。incを使うことで、ページ数の書き込みが競合しても上書きされない。

initialize

embedのモデルは、親への参照を持ち、自身のレコードが変更された際は、親(Mongoドキュメントオブジェクト)のchangesにその内容を反映させる。

bookshelf.rb

#coding: utf-8
require 'mongorilla'
require File.expand_path("./book_info", File.dirname(__FILE__))
class Bookshelf
  BookshelfFields = [:_id,:user_id,:book_num,:book_info_records]
  include Mongorilla::Document

  alias save_orig save

  def self.build(user_id)
    Bookshelf.create( :user_id => user_id,
              :book_num => 0,
              :book_info_records => []
              )
  end

  def book_infos
    self.book_info_records.map{|r| BookInfo.new(self,r)}
  end

  def book_info(book_id)
    rec = self.book_info_records.find{|r| r["book_id"] == book_id}
    return nil unless rec
    BookInfo.new(self,rec)
  end

  def add_book(book_id)
    @new_books ||= []
    @new_books.push(book_id)
    BookInfo.add(self,book_id)
  end

  def update_book(rec)
    book_info(rec.book_id).update(rec.page_num,rec.updated_at)
  end

  def save_new(condition={},opt={})
    if @new_books
      self.inc("book_num",@new_books.count)
      save_orig(condition.merge!({"book_info_records.book_id" => {"$nin" => @new_books}}),opt)
    else
      save_orig(condition,opt)
    end
  end
  alias save save_new
end

self.build

デフォルト値をセットするために、呼び出し元にcreateではなく、buildを呼んでもらうようにする

book_infos

book_infosはbook_info_recordsのArrayを使ってModelを作成し、Bookshelfと1対多の内部リレーションを形成。 Bookshelfの使用元には、book_info_recordsのフォーマットを意識させない。Arrayではなく、Hashでもeachを 使いほぼ同じ処理で実現できる。

book_info

指定したbook_idの本があれば、Modelにマッピングして返却。なければnilを返すのでその本を所有しているかの 調査にも使える。

add_book

Bookshelfに本を追加する際のメソッド。本自体の情報はBookInfoが持つためBookInfoに移譲しているが、複数の本を 追加した際に何冊追加したか、また登録済の本を更新しない条件をsave時に認識できるように 便宜的にnew_booksと いう配列に追加した本のIDを登録

update_book

recの内容を同じidを持つbook_infoに更新するよう移譲

save_new

本の追加時にすでに別プロセス等により登録されている本を追加できないようにするため、saveに条件を指定している。 bookshelfのsaveの呼び出し側はそのことを意識しないですむようにするため、saveをwrapperしている。 book_numのレコードは、配列のサイズは$size => 5などの等比の条件はできるが、$gtなどの比較は使えないため便宜的に 配列のサイズを別レコードで持つことで、 $ltを使えるようにして、要素が10個未満の場合は登録できるようにするなどの 処理が行えるようになる。

book_info.rb

#coding: utf-8
require File.expand_path("./book_info_fields", File.dirname(__FILE__))
require File.expand_path("./bookshelf", File.dirname(__FILE__))
class BookInfo
  include BookInfoFields

  def update(page_num,updated_at)
    idx = @bookshelf.book_info_records.index{|r| r["book_id"] == @info["book_id"] }
    @bookshelf.changes["$set"] ||={}
    @bookshelf.changes["$set"]["book_info_records.#{idx}.page_num"] = page_num
    @bookshelf.changes["$set"]["book_info_records.#{idx}.updated_at"] = updated_at
    @info["page_num"] = page_num
    @info["updated_at"] = updated_at
  end

  def self.add(bookshelf,book_id)
    bookshelf.changes["$pushAll"] ||= {}
    bookshelf.changes["$pushAll"]["book_info_records"] ||= []
    if bookshelf.origin["book_info_records"].find{|r| r["book_id"] == book_id}
      raise "already exists!!"
    end
    if bookshelf.changes["$pushAll"]["book_info_records"].find{|r| r["book_id"] == book_id}
      raise "already push!!"
    end
    now = Time.now
    record = {"book_id" => book_id,"page_num" => 0,"created_at" => now,"updated_at" => now}
    bookshelf.changes["$pushAll"]["book_info_records"].push(record)
    bookshelf.book_info_records.push(record)
  end

  def initialize(bookshelf,rec)
    @bookshelf = bookshelf
    @info = rec
  end
end

update

page_num,updated_atの内容をbook_infoに更新し、save時にDBに反映されるようにしている。配列の更新は.添字.フィールドで アクセスできる。

self.add

更新時に、複数のbook_infoが登録できるように$pushAllを使用している。Mongoドキュメントオブジェクトには、originというメソッドで DBから取得した時点でのレコードにアクセスできる。

initialize

embedのモデルは、親への参照を持ち、自身のレコードが変更された際は、親(Mongoドキュメントオブジェクト)のchangesにその内容を反映させる。

reading_log.rb

#coding: utf-8
require 'mongorilla'
class ReadingLog
  ReadingLogFields = [:_id,:book_id,:user_id,:page_num,:created_at]
  include Mongorilla::Document
end

bookshelf_spec.rb

#coding: utf-8
require 'rspec'
require 'logger'
require File.expand_path("./user", File.dirname(__FILE__))

describe  Bookshelf do
  before do
    Mongorilla::Collection.build(File.expand_path("./mongo.yml", File.dirname(__FILE__)))
    @user = User.build(:name => "morita")
  end

  context "bookshelf" do
    it{@user.bookshelf.should_not be_nil}
  end

  describe "本を追加(正常)" do
    before do
      @user.bookshelf.add_book("1")
      @user.bookshelf.add_book("2")
      @user.bookshelf.save
      @bookshelf = Bookshelf.find(@user.bookshelf.id)
    end
    it{ @bookshelf.book_infos.map(&:book_id) =~ ["1","2"]}
    it{ @bookshelf.book_num.should == 2}
  end

  describe "本を追加(異常)" do
    before do
      @user.bookshelf.add_book("1")
      @bookshelf = Bookshelf.find(@user.bookshelf.id)
      @bookshelf.add_book("1")
      @user.bookshelf.save
      @ret = @bookshelf.save
      @bookshelf.reload
    end
    it{ @ret.should == false}
    it{ @bookshelf.book_infos.map(&:book_id) =~ ["1"]}
    it{ @bookshelf.book_num.should == 1}
  end

  describe "読書結果を反映" do
    before do
      @user.bookshelf.add_book("1")
      @user.bookshelf.add_book("2")
      #本の追加を更新
      @user.bookshelf.save
      @user.current_book_info.change(@user.bookshelf.book_infos[0])
      #手元の本をBookID1の本にする
      @user.save
      #本を10ページ読みすすめる
      @page_num=10
      @user.inc("total_read_page_num",@page_num)
      @user.current_book_info.inc(@page_num)
      @user.save
      @user.reload
    end
    it{@user.total_read_page_num.should == @page_num}
    it{@user.current_book_info.page_num.should == @page_num}
    it{@user.current_reading_logs.count.should == 1}
    describe "本を変更" do
      before do
        @user.bookshelf.update_book(@user.current_book_info)
        #本棚の本の情報を手元の本の情報で更新
        @user.bookshelf.save
        @user.current_book_info.change(@user.bookshelf.book_infos[1])
        #手元の本をBookID2の本にする
        @user.save
        @user.reload
      end
      it{@user.current_book_info.page_num.should == 0}
    end
  end
  after do
    User.remove()
    Bookshelf.remove()
  end
end

mongo.yml

database: bookshelf

BookMaster.yml(未使用)

"1":
  author: "Robert C. Jacke"
  title: "Coop Save money"
"2":
  author: "中島かも"
  title: "イベリコ豚"
マスターデータは、JoinがないMongoDBの場合、静的ファイルで持ち、アプリ起動時に読みこむようにするなど、DBに登録しないほうがいい。

まとめ

上記のアプリを仕様検討も含め、5時間弱で作成できたので、Mongorillaは記述しやすいミドルウェアだと思います。 アプリでリレーションを管理することにより、煩雑になっている部分はありますが、その変わりDBへのアクセス数を減らし、 不整合を起こさないような作りができます。

| | コメント (0) | トラックバック (0)

MongoDBでのリレーションのパターン

1対1の外部リレーション

embedの機能があるMongoDBで1対1の外部リレーションを使いたい場合というのは、そんなにないと思います。 しいて挙げるなら、あまり参照しないがデータサイズが大きいため、毎回読み込むのは避けたい場合ぐらい。 ただ、その場合でもfind時に必要なフィールドのみを取得できるオプション があり、それを使えば1対1で外部リレーションさせた場合と同じ効果が得ら れます。

1対多で他Documentとのリレーション

ログ等無限にどんどん増えるデータは、embedには向かないため、外部リレーションの関係になります。また細かい条件で検索したいレコードも 外部リレーションの1対多となります。例えば、10個以上20個未満のアイテムを探したいと思い、 find({:item.num => {"$gte" => 10,"$lte" => 20}})としても、1レコード全体を対象にするため、下記のレコードがHitしてしまいます。
{:item => [{:num => 5,:name => "りんご"},{:num => 25,:name => "みかん"}]}

1対1の内部リレーション

Atomicに扱いたいものの、直接のAttributeにしたくはないような レコードの場合。FiledをHashにして管理します。

1対多の内部リレーション

無限に増えそうもないデータ(持ち物情報等)は、embedにすることで、 Atomicに更新ができ、クエリも1回でデータを取得できます。
フィールドがArrayで値をHash形式で持つパターンとフィールドがHashで値もHash形式にするパターンとがあります。

フィールドがArrayのパターン

indexが貼りやすい反面、配列のデータの更新と追加が同時にできません。
例えば {name:"takeshi",age: 35, children: [{name: "takeru" ,age: 18}]}の レコードに対し、name:"takeru"のnumを19に変更すると同時に{name: "yui",num: 2}の レコードを追加といったことがatomicにできません。
indexは簡単で、[[children.name,Mongo::ASCENDING],[children.age,Mongo::DESCENDING]] を貼ることでfind({:children.name => "takeru",:children.age => 18})のような検索にindexを使います。
更新はchildren.0.ageのように添字を使って更新するため、削除があるようなレコードには あまり向いていません。他のプロセスが削除を行なった場合、添字で指定したレコードが 違うレコードを指す可能性があるためです。ただしArrayの内、1つのレコードのみ更新したい場合は、update({:children.name => "takeru"},{"children.$.age" => 19})のように検索条件で一致したレコードの添字の変わりに$が使えます。
削除は$pullを使いますが、一致するレコードが複数件存在する場合は、一致するものすべて削除されてしまうため、一致するレコード1件のみ削除したい場合は、$unsetで一致する レコードをnullにした後、$pullでnullのレコードを削除するという2段階踏む必要があります。

フィールドがHashの場合

レコードの更新の追加がatomicにできるのに対し、indexが各項目に対して作成する必要があります。 Hashの場合は、{name: "takeshi,items: {りんご: 1}}というレコードの値を2にすると同時 に{みかん: 1}のレコード追加が同時にAtomicできます。 ただし、indexは[["items.りんご",Mongo::ASCENDING],["items.みかん",Mongo::ASCENDING]] のように、アイテムの種類が増えればその度にindexを作成する必要があります。 削除は、$unsetを使用します。

まとめ

1対多のEmbedをArrayにするかHashにするかはよく悩みます。
基本的には、削除がなく、レコードの更新と追加が同時に必要にならないレコードであれば Arrayにし、削除があったり、レコードの更新と追加が同時に必要な場合はHashにするといいと思います。
ただ、indexの兼ね合いもあり、削除があっても論理削除をすることによって、 Arrayでも更新の不整合を防ぐことができます。
長くなったので、Mongorillaを使ったリレーションの管理は次回。

| | コメント (0) | トラックバック (0)

2011年11月27日 (日)

Mongorilla: Object Document Mapper for MongoDB

Mongorillaについて

Mongoidの弱点を克服するために作成したObject Document Mapper.
名前の由来は、Mongoと娘の好きな動物のgorillaを掛けあわせた。

インストール

gem install mongorilla

ソース
github

長所

  1. 今のところ、わずか300行ちょっとの小ささ。
  2. シングルサーバ、Master/Slave、ReplicaSetに対応。
  3. 単純な処理はMongoidと変わらず、複雑な処理も対応。
  4. クエリ発行時に接続失敗しても、指定回数リトライできる。
  5. findクエリに対してMasterとSlaveのどちらを参照するか指定できる。

短所

  1. リレーション関連は自分で管理する必要がある。
  2. 各フィールドの型も自分で整合性を保証する必要がある。

sample

require 'mongorilla'
# クラス名をunderscore化+複数形にしたcollectionが作成される
class User
  # クラス名+ Fieldsの定数に使用するフィールドをsymbolで定義
  UserFields = [:_id,:name,:password,:logs,:log_count]
  # Mongorillaを使うことを宣言
  include Mongorilla::Document
end
#アプリ起動時1回のみ呼び出す必要(設定ファイルに基いてDBに接続している)
Mongorilla::Collection.build(File.expand_path("../config.yml",__FILE__))
#ユーザデータをDBに登録し、登録したレコードを呼び出す
user = User.create(:name => "morita",:password => "pass")
#非同期で作成したい場合
User.create({:name => "morita",:password => "pass"},{:safe => false})
#複数ユーザ同時に作成したい場合
users = User.create([{:name => "morita",:password => "pass"},{:name => "yoshida",:password => "hello"}])
#IDで検索する場合 1オブジェクトのみ返る
u = User.find(user.id)
#属性で検索する場合 条件に一致したオブジェクトすべてがArrayで返る
us = User.find({:name => "morita"})
#オプションをつける。 :master => trueのみ独自(master DBから検索)。
#それ以外はmongo driverに等しい。
us = User.find({:name => "morita"},{:master => true,:limit => 3})
#件数を調べる。(find.countだとレコードをすべてObjectに変換した後、countしてしまうため)
count = User.count({:name => "morita"})
#属性の変更 DBにはsaveを実行するまで更新されない。 
u.name = "mora"
#属性の変更 DBにはsaveを実行するまで更新されない。
u.push("logs","oooo")
#属性の変更 DBにはsaveを実行するまで更新されない。p u.logs => ["oooo","aaa"]
u.push("logs","aaa")
#属性の変更 DBにはsaveを実行するまで更新されない。p u.log_count => 2
u.inc("log_count",2)
#上記の変更がDBに反映される(デフォルトは同期)
u.save
#属性の変更 DBにはsaveを実行するまで更新されない。p u.log_count => 1
u.inc("log_count",-1)
#条件を指定して更新。この場合は条件を満たさないため失敗
ret = u.save({:log_count => {"$gt" => 10}})
#false
p ret
# 2 失敗した場合は、元のデータに戻される
p u.log_count
# $pullの実行 ただしpullの場合はオブジェクトに反映されない(複雑だから。) p u.logs => ["oooo","aaa"]
u.pull("logs","oooo")
# 上記に合わせて、件数を減らす
u.inc("log_count",-1)
# RELOADの指定は、レコードの更新が成功した場合に、更新したレコードをDBから取り出して、
# オブジェクトの内容を更新する。$pullを使用した場合はオブジェクトとDBのレコードの間に
# 不整合ができるため、行うほうがいい。RELOAD以外は、SYNC(デフォルト)、ASYNC(非同期)がある
ret = u.save({:log_count => {"$gt" => 1}},Mongorilla::Document::RELOAD)
# pullをしてreloadしたことにより、"oooo"が取り除かれた。["aaa"] 
p u.logs

設定ファイル

Mongorilla::Collection.buildに渡す設定ファイル(yaml形式)のフォーマット

Single Serverの場合

host: localhost #任意
port: 27017     #任意
database: dev   #必須

Master/Slaveの場合

host: localhost     #必須
port: 27017         #必須
database: dev       #必須
slaves:             #必須
  - host: localhost #必須
    port: 27018     #必須


Replica Setの場合

hosts:
  - - localhost #HOST
    - 27017     #PORT
  - - localhost
    - 27018
  - - localhost
    - 27019
database: dev

内部実装について

Mongorilla::Documentをinclude指定された際に、指定されたフィールドごとにgetterとsetterを動的に作成している。
getterはメソッド呼びだし時にinitialize時に渡されたMongoのレコード(@docに保存)に対してフィールド名でアクセスした結果を返している。
setterは@changesに@changes["$set"][フィールド名] = 変更された内容をセットして、@docの内容を更新している。
saveを呼び出した時は、@changesの内容を用いてupdateしている。
複雑な更新をしたい場合は、Obj#changesで@changesを取得して変更し、@docの内容を更新する。
フィールドにセットするデータをチェックしたい場合は、def フィールド名= のメソッドをオーバーライドする。
リレーションの管理方法は、また今度。

| | コメント (0) | トラックバック (0)

Mongoidの限界について

Mongoidの限界

  1. 条件指定のupdateができない。
  2. $incと$pushが同時に使えない。
  3. Hash,Arrayのフィールドに対して、一部のキーのみの更新ができない
  4. save時に参照する必要のないHas_oneの関連のクエリを発行
  5. 変更したオブジェクトに対してreloadを実行すると不整合になる

条件指定のupdateができない

アイテム購入時に、金額が$200以上保持している場合のみ、アイテムを付与するといった
ことができない。アプリケーションが1プロセスの場合は、事前に取得したレコードの金額
を比較することで対処できるが、複数プロセスの場合は、事前に取得したレコードの内容
と更新時のレコードの内容が等しいとは限らないため、更新時に$200以上の場合のみアイテム
付与といったことをMongoDBでする必要がある。

$incと$pushが同時に使えない

Arrayでデータを保存していて、配列のサイズが10以下の場合のみ更新するという場合、
配列のサイズをに対して$gtや$ltなどの比較演算子が使えない($sizeで一致の場合は使える)
ため、Arrayのサイズを記憶するフィールドを別に持つ必要があり、その場合、$pushと$incを
同時に発行する必要がある。

Hash,Arrayのフィールドに対して、一部のキーのみの更新ができない。

{'a' : 1}の値があるフィールドに'b' : 2のデータを追加して更新すると{'a' : 1,'b': 2}とすべての
データで更新してしまうため、他のプロセスが更新したデータを上書きしてしまう。

save時に参照する必要のないHas_oneの関連のクエリを発行

データサイズを小さくするために、あまり参照されないデータは、has_oneの関係で外出しに
するが、関連元のレコードをsaveする際に、外出しにしているhas_oneのレコードにfindの
クエリを発行するため、せっかく外出しにした意味がなくなる。

変更したオブジェクトに対してreloadを実行すると不整合になる

上記の理由により、MongoDriverから直にデータをDBを更新し、Mongoidのオブジェクトを
整合性を取るために更新した内容にセットした状態で、そのオブジェクトにreloadを実行すると

同じレコードが重複したりする。

まとめ

MongoidはActive Modelをincludeしていて、ActiveRecordに慣れたユーザが簡単に導入できるという利点がある。 ただ、高負荷がかかり、データの不整合が許されないような現場で使用するには問題がある。 その対応策として、MongorillaというObject Document Mapperを自作した。 次回はその紹介をしたい。

| | コメント (0) | トラックバック (0)