0

またHubotのbrainが爆発したのでhubot-mongodb-brain作った

既存のbrainがあまりにもひどいので自作した。

https://www.npmjs.com/package/hubot-mongodb-brain
https://github.com/shokai/hubot-mongodb-brain

npm installして、external-scripts.jsonに書けば使える。何も設定しなくてもローカルのmongodbか、Herokuならmongolabかmongohqを読み込む。

他のbrainからの移行スクリプトもある。


爆発

以前爆発した時
hubotのbrainが爆発した

hubot-brain-redisは全データを1つのblobとして固めて保存するから、brainのサイズが1.5MBを超えるとRedisToGoのmax memoryにひっかかって保存できなくなる。
そこでhubot-brain-redis-hashに乗り換えたのだが、やっぱり2MB超えたあたりで保存できなくなった。brain-redis-hashもよく見たら1つのblobにまとめて1keyに保存してた。READMEには1つのblobとしては保存しないって書いてあるけど、よくコード読んだら

// brainのデータ構造
{
users: userとroomのリスト
_private: { // この中をrobot.brain.get/setで操作している
key1: value1,
key2: value2,
key3: value3
(略)
}
}
これのusersと_privateを別に保存してるだけなので、全然解決してない。

_privateの中をkey毎に保存するべきで、usersはそもそも用途が無いので保存する必要がない。
hubot-mongodb-brainでは_privateだけ保存するようにした。

他の選択肢

mongo-brainとmongolab-brainも、1 documentに全部固めて保存するのでだめ。そもそもcollections.find().limit(1)とかcollections.find({})とかで取り出してるのが気持ち悪すぎる。

0

mongooseにschema関係なくなんでも保存する

mongooseはスキーマで宣言されていないkey:valueは保存してくれない。MongoDBっぽくないけど安全ではある。
でも、webhookで色々なjsonがサーバープッシュされてくるAPIのデータを全部保存したい場合に困る。例えばtwitterやjawbone upのstream APIとかを、mongooseでスキーマレスになんでも保存したい。


方法1 Mixedを使う


調べると、Schema.Types.Mixedを使えとか書いてある。

var Any = new Schema({ any: Schema.Types.Mixed });
ただこの方法だと、なんでも保存できるanyの中に保存しろって事なんだけど、documentの中の深い部分を検索する事になってめんどい。


方法2 mongo driverを直接使う


mongoose.connections配列がnode-mongodb-nativeなので、ここに直接insertすればschema無いデータも保存できる。

例として、jawbone-up24がwebhookでプッシュしてくるjsonを全部eventsというcollectionに保存する。


model定義

## 空のschemaを作る
eventSchema = new mongoose.Schema

## 直接insert
eventSchema.statics.insert_webhook = (data, callback = ->) ->
mongoose.connections[0].collection('events').insert data, callback

mongoose.model 'Event', eventSchema
スキーマ関係なく直接保存するメソッドをmongooseのmodelに生やした。
mongooseのmodel Eventにスキーマ登録すると、mongoでは複数形のevents collectionになる


expressでwebhook受信してmongo driverに直接保存

Event = mongoose.model 'Event'
app.use bodyParser.json()

app.post '/webhook', (req, res) ->

## jawboneはevents[0]にデータが入ってるので、それを保存する
Event.insert_webhook req.body.events[0], (err, res) ->
console.log "保存した" unless err

return res.end "ok"


この方法だとcontroller側から使う部分もmongooseっぽいし、query投げるのもmongoose風に普通に書ける

eventSchema.statics.last_move_of_user = (user_id, callback) ->
@find
user_xid: user_id
type: 'move'
.sort
timestamp: 'desc'
.limit 1
.exec callback

0

mongooseのvirtual attributes

Query投げて返ってきたDocumentsが関数を持っていて欲しい時にVirtual Attributes使う。

  • スキーマ.virtual(‘名前’).get(function(){ /**/ })
  • スキーマ.virtual(‘名前’).set(function(){ /**/ })
でgetter/setterを付けれる。

例えば、MongoDBにはidしか保存しないようにして、permalinkのURL等はidから組み立てる場合、そういう関数はmodelに持たせたい。


実装例

RecipeSchema.virtual(‘url’).get に関数を登録しておくと、 doc.url で呼び出せる。

mongoose_virtual_attr.js
var _ = require('underscore');
var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/test', function(err){
if(err){
console.error(err);
process.exit(1);
}
});

var Schema = mongoose.Schema, ObjectId = Schema.ObjectId;

var RecipeSchema = new Schema({
id : {type: String, unique: true},
title : {type: String},
created_at : {type: Date, default: Date.now}
});
RecipeSchema.virtual('url').get(function(){
return 'http://shokai.org/'+this.id;
});

var Recipe = mongoose.model('Recipe', RecipeSchema);
Recipe.latests = function(num){
return this.find().sort('created_at', -1).limit(num);
};

var recipe = new Recipe({id: '29jg', title: '肉じゃが'});
recipe.save();

Recipe.latests(3).exec(function(err, docs){
if(err){
throw err;
}
else{
_.each(docs, function(doc){
console.log(doc.url);
});
}
mongoose.disconnect();
});


% npm install mongoose underscore
% node mongoose_virtual_attr.js
http://shokai.org/29jg


というのがやり方わかんないんだよねぇ・・と相談したらmyatsumoto氏が教えてくれた。

0

HerokuでMongoDB+Node.js

HerokuでMongoDB+Sinatraと同じ、1行メモ的なものをNode.js+Express+mongooseで作った。


mongooseはmongoidと同じ雰囲気のQueryチェインが使えて便利。


MongoDBに接続、スキーマ定義

アドオンのMongoLabかMongoHQを入れてたらそっちに接続するようにした。
var mongoose = require('mongoose');
mongoose.connect(process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost/memo', function(err){
if(err){
console.error(err);
process.exit(1);
}
});

var Schema = mongoose.Schema, ObjectId = Schema.ObjectId;

var MemoSchema = new Schema({
body : {type: String},
created_at : {type: Date, default: Date.now}
});

var Memo = mongoose.model('Memo', MemoSchema);

Memo.latests = function(num){
return this.find().sort('created_at', -1).limit(num);
};

Memo.find_by_id = function(id){
return this.find({'_id':id});
};


読み書き

modelのオブジェクト作ってsaveするとMongoDBに保存される。
var mongoose = require('mongoose');
var Memo = mongoose.model('Memo');

var m = new Memo({body: "はい"});
m.save();

代入しても良い。Schema定義した時にgetterが生成されている。
var m = new Memo();
m.body = "はい";
m.save();


Queryを繋げて書けて良い。
var _ = require('underscore');
var mongoose = require('mongoose');
var Memo = mongoose.model('Memo');

Memo.where(body: /はい/).sort('created_at', -1).limit(30).exec(function(err, docs)){
if(err){
console.error('error');
}
else{
_.each(docs, function(i){
console.log(i.body + ' - ' + i.created_at);
});
}
});


さっきschema定義の時にmodelに付けておいたメソッドを使う
Memo.latests(3).exec(function(err, docs){
if(err) console.error(err);
else{
_.each(docs, function(d){
console.log(d.body + ' - ' + d.created_at);
});
}
});

データの出し入れはcontrollerviewのコードが参考になるかも。
async.parallelで2つのクエリを同時に投げて結果待ちしたりしてみた。なんとなく速そう。


jadeとhamlのちがい

にはまった。

この部分

hamlだとこう書くけど
%ul
- memos.each do |memo|
%li
#{memo.body} -
%a(:href => "/#{memo.id}") #{memo.created_at}


jadeだとliと同じ行に書かないと、下のaタグの中身の変数(memo.created_atとか)が展開されなくなる。
ul
- each memo in memos
li #{memo.body} -
a(href="/#{memo.id}") #{memo.created_at}

0

HerokuでMongoDB+Sinatra

試しにメモ帳作った。
http://shokai-memo.herokuapp.com

ソースコード https://github.com/shokai/heroku-sinatra-mongo-memo


MongoHQMongoLabを使う。

MongoHQはHerokuが管理してるっぽくて16MBまで無料。MongoLabはObjectLabs Corporationがやってて240MBまで無料らしい。
MongoLabの方が明らかに得だけど、容量を2GBまで増やすとMongoHQの方が安くなる。


Herokuのプロジェクト作る

git init
heroku create --stack cedar


MongoDBのアドオンを有効にする

とりあえず無料プランで、どちらかをaddする
heroku addons:add mongolab:starter
heroku addons:add mongohq:free

するとMONGOLAB_URIかMONGOHQ_URLという環境変数が増えるので、確かめる
heroku config


RubyではMongoid2.4をMongoのラッパーとして使うのが良いと思う。
Herokuにデプロイしたら環境変数にMongoDBのアドレスとパスワード等が入った状態でSinatraが起動するので、読み込ませる
Mongoid.configure do |conf|
conf.from_hash {'uri' => ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL']}
end


最終的にこうなった。
mongoidは普通mongoid.ymlから設定を読む。
MongoLabやMongoHQの接続設定が環境変数にあったらそっちを使うし、無ければmongoid.ymlを読んでローカルのMongoDBに接続する。
Mongoid.logger.level =
case ENV['RACK_ENV']
when 'production'
Logger::WARN
else
Logger::DEBUG
end

Mongoid.configure do |conf|
h = {'uri' => ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL']}
unless h['uri']
yaml = YAML.load(open(File.expand_path 'mongoid.yml', File.dirname(__FILE__)).read)
h = yaml[ ENV['RACK_ENV'] || 'development' ]
end
conf.from_hash h
end



メモ帳作る

Memo modelを定義する
class Memo
include Mongoid::Document
field :created_at, :type => Time, :default => lambda{ Time.now }
field :body, :type => String, :default => ""

def self.find_by_id(id)
self.where(:_id => id).first
end

def self.latests(num=10)
self.all.desc(:created_at).limit(num)
end

def to_s
"#{body} - #{created_at}"
end
end


適当にSinatraで書いて完成。
get '/' do
mems = Memo.latests(100).map{|m|
"<p>#{Rack::Utils.escape_html m.body} - <a href='/#{m.id}'>#{m.created_at}<a><p>"
}.join('')

"<html><form method='POST' action='/'><input type='text' name='body' size=70 /><input type='submit' /></form>#{mems}</html>"
end

post '/' do
m = Memo.new :body => params[:body]
m.save!
redirect '/'
end

get '/:id' do
m = Memo.find_by_id params[:id]
halt 404, 'not found' unless m
"<html><p><a href='/'>top</a></p><p>#{Rack::Utils.escape_html m.to_s}</p></html>"
end


ローカルのRubyからHerokuのMongoDBに接続する


手元で起動したSinatraも、環境変数さえセットされていればHerokuのMongoDBに接続される
heroku config --shell | ruby -lane 'puts "export "+$_'
でてきたMONGOLAB_URIとかをターミナルにコピペすれば環境変数をセットできる。


管理画面

https://api.heroku.com/myappsからアプリを選んで、右上のaddonsからMongoLab/MongoHQの管理画面に行ける。