〜ハッカソンにて〜
「Node.js使ったら簡単にDBも操作できるらしい」
→「なんだよこれ全然わかんねえよ」
Node.jsとMongoDBを使ってjsonサーバをつくろうとしたけど,
いろんな罠があったのでメモしときます.
初めてNode.jsやjson,MongoDB使うよって人向けです.
基本的には以下のサイトを参考にして作ってます.
- Node.js + Express 4.x + MongoDB(Mongoose)でRESTfulなjsonAPIサーバの作成を丁寧に解説する.+ テストクライアントを用いたAPIテストまで - Qiita
- ゼロからはじめるExpress + Node.jsを使ったアプリ開発 - Qiita
- node.js から MongoDB にアクセス (Mongoose の紹介) - KrdLab's blog
- Mongoose の Document インスタンスにプロパティを追加しての JSON 化 - とりあえず備忘録としてはじめようかな?
今回つくるもの
フロントエンドがjson形式でユーザ名+password/APIで取得したキーを送信してくるので,
それらをDBにぶち込んで参照したりupdateしたりフロントエンドに返したりするサーバを構築します.
今回フロントエンド側が使用するAPIはご当地キャラカタログです.
フロントエンドはご当地キャラカタログから取得したキャラクターIDをサーバにjson形式でPOST送信します.
環境
- Ubuntu 14.04 LTS
- Node.js v4.4.7
- express v4.x
- MongoDB v2.2.11
- Mongoose v2.15.8
Ubuntu上で開発していますが,基本的にコマンドが叩けたらMacでもCentOSでも何でもいいと思います.
開発の準備
各種インストール
インストールすべきものは以下
- Node.js v4.4.7
- express v4.x
- MongoDB v2.x
- Mongoose v2.x
いろんなサイトでインストールの仕方が紹介されてるので詳細は割愛.
そんなんいちいち見るの面倒だよって人向けにシェルスクリプト作ったので使ってください.(動作確認してないけど)
https://github.com/tdomen/node.js_mongodb_server/blob/master/nodejs_installer.sh
ディレクトリ構成
今回は以下のような構成で開発を行います.
$ tree .
backend
|
|--app.js
|--package.json
|--nodejs_installer.sh
|--models
| |--user.js
| |--zukan.js
|--routes
|--auth
| |--login.js
|--zukan
|--zukan.js
$ npm init
しとけばだいたい上のディレクトリ構成になると思うので,
適宜必要なファイルやディレクトリを追加していってください.
MongoDBの起動・設定
MongoDBの起動には以下のコマンドを叩きます.
$ sudo mongod --dbpath /var/db/mongo/
これでDBサーバが起動します.
MongoDBは対話型のシェルで操作可能です.
$ mongo
コマンドでmongoシェルが起動します.
今回は「backend」というデータベースを作成し,
その中にコレクション(テーブル)を入れていきます.
今回は「users」,「zukans」の2つのコレクションを使用します.
データベース作成およびコレクション作成はmongoシェル上で以下のコマンドを打つことで可能です.
$ mongo
>use backend
>db.createCollection('users')
{ "ok" : 1 }
>db.createCollection('zukans')
{ "ok" : 1 }
上のコマンドを実行した後,「show collections」コマンドによって
データベース内のすべてのコレクションを見ることができます.
>show collections
system.indexes
users
zukans
「users」コレクションと「zukans」コレクションの2つが作成されていればOKです.
(system.indexesは最初からあるみたいです.)
確認ができたら「quit()」コマンドでmongoシェルを終了します.
以上で開発の下準備は終了です.
サーバ実装
今回の実装の流れは次の通りです.
- ユーザ認証用DBのスキーマ定義
- app.jsの追記
- routes/auth/longin.jsの実装
- APIによる取得キー用DBのスキーマ定義
- app.jsの追記
- routes/zukan/zukan.jsの実装
流れを見ても分かるとおり,基本は1. スキーマ定義,2. app.jsの追記,3. ルーティング先の実装,の3ステップです.
それではまずユーザ認証の機能を実装していきます.
ユーザ認証機能の実装
本項ではユーザ認証の機能を実装します.
フロントエンドはPOSTリクエストによって認証を要求します.
リクエスト内容はJSON形式で記述されています.
{
"username" : String
"password" : String
}
サーバはusernameでDBのusersコレクションを参照し,
usernameが存在した場合はpasswordと照合してその結果をフロントエンドに返します.
usernameが存在しなかった場合,usernameとpasswordを新規登録します.
1. ユーザ認証用DBのスキーマ定義
スキーマ定義は/modelsの中に定義します.
今回,ユーザ認証用のスキーマは/models/user.jsに記述します.
// /backend/models/user.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var User = new Schema({
username : { type: String, require: true, unique: true },
password : { type: String, require: true }
});
module.exports = mongoose.model('user', User);
usernameはメールアドレスを想定しているため,uniqueにしています.
2. app.jsの追記
app.jsに,後に作成するlogin.jsをルーティング先に指定します.
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
// ---- Renamed and Added by tdomen -----
var login = require('./routes/auth/login');
var http = require('http');
var mongoose = require('mongoose');
// ----------------------------------------
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
// ---- Added by tdomen -----
// ポート設定
app.set('port', process.env.PORT || 3000);
// --------------------------
app.use(logger('dev'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// ---- Added by tdomen -----
// ルーティング機能
app.use('/login', login);
// --------------------------
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
// サーバ立ち上げ
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
mongoose.connect('mongodb://localhost/backend');
});
// ------------------------------------------
module.exports = app;
フロントエンド側から「http://〜:3000/login」というリクエストがきた場合,
ルーティング先に指定したroutes/auth/login.jsにて処理されます.
app.jsの編集はとりあえず終わりです.
3. routes/auth/longin.jsの実装
login.jsではフロントエンドから来たPOSTリクエストを処理します.
POSTリクエストでフロントエンド側から送信されたusernameキーでDB内を検索し,
検索に引っかからなかった場合はusernameとパスワードで新規登録を行います.
検索に引っかかった場合は,DB内のパスワードとリクエスト内容を照合し,
一致していればtrueを,そうでなければfalseをフロントエンドに返します.
var express = require('express');
var bodyParser = require('body-parser');
var router = express.Router();
var mongoose = require('mongoose');
// モデルの宣言
var User = require('../../models/user');
// POSTリクエストがきた時の処理
/* POST内容はJSON形式で飛ばされる
* JSON format
* {
* "username" : String
* "password" : String
* }
*/
router.post('/', function(request, response){
console.log("catch the post request");
response.setHeader('Content-Type', 'text/plain');
// パラメータ名、usernameとpassを出力
console.log(request.body.username);
console.log(request.body.password);
var username = request.body.username;
var password = request.body.password;
User.find({ "username" : username }, function(err, result){
if (err)
console.log(err);
// 新規登録
if (result.length == 0){
var user = new User();
user.username = username;
user.password = password;
user.save(function(err){
if (err) console.log(err);
response.send("new_created");
});
}
// usernameがDBに存在した場合
else{
if (result[0].password == password)
response.send("true");
else
response.send("false");
}
});
});
module.exports = router;
User.findにて,第一引数に検索するusernameキーとPOSTリクエスト内のusernameを取ります.
その結果は第二引数の無名関数function(err, result)
の第二引数に格納されます.
result.lentghが0であった場合,検索条件に一致せず,
DB内にリクエスト内容のusernameが存在しないことを意味するので,新規登録のフローに入っていきます.
そうでなかった場合は,result配列の0番要素
(スキーマでusernameをunique指定しているため,必ず要素は1つのみとなるはず)の
passwordとリクエスト内passwordを照合し,その成否をフロントエンドに返却しています.
以上でユーザ認証機能は実装終了です.
(今回はセッション管理等は行っていません.そのあたりを知りたい人はNode.js+Express+MongoDBでSessionを利用してログイン機能を実装 - Qiitaを参考にしてみてください.)
キャラクターIDをユーザごとに保存/取得する図鑑機能の実装
本項ではフロントエンドから送られるキャラクターIDを
ユーザごとに保存,取得する図鑑機能を実装します.(ポケモン図鑑のイメージです)
フロントエンドはGET/POSTリクエストによって,今まで保存してきたIDを取得するか,
DBに保存する要求を行います.
DBに保存を要求するPOSTリクエスト内容はJSON形式で記述されています.
今回,フロントエンドはご当地キャラカタログで取得した
JSONをサーバにそのまま渡します.
{
"username" : String
"characterID" : String
}
サーバはusernameでDBを参照し,POSTリクエスト内のcharacterIDを追加登録します.
1. 図鑑用DBのスキーマ定義
スキーマ定義はユーザ認証用と同じく,/models直下にファイルを置きます.
今回,図鑑用のスキーマを/models/zukan.jsに記述します.
// /backend/models/zukan.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var Zukan = new Schema({
username : { type: String },
characterID : { type: [Number] }
});
module.exports = mongoose.model('zukan', Zukan);
usernameは本来,ユーザ認証用のDBと組み合わせておくとよいです.
DB設計はちゃんとやるべきですが,まあ今回はハッカソンだしいっかの精神で割愛しました.
characterIDは数値の配列でスキーマ定義しています.
この配列部分が後にすごく苦しむことになったんですがね.
2. app.jsの追記
さきほどと同様,app.jsに,後に作成するzukan.jsを用いることを宣言します.
// 割愛
// ---- Renamed and Added by tdomen -----
var login = require('./routes/auth/login');
var zukan = require('./routes/zukan/zukan');
var http = require('http');
var mongoose = require('mongoose');
// ----------------------------------------
// 割愛
// ---- Added by tdomen -----
// ルーティング機能
app.use('/login', login);
app.use('/zukan', zukan);
// --------------------------
// 割愛
フロントエンド側から「http://〜:3000/zukan(:username)」というGET/POSTリクエストがきた場合,
ルーティング先に指定したroutes/zukan/zukan.jsにて処理されます.
これでapp.jsの編集は終了です.
3. routes/zukan/zukan.jsの実装
zukan.jsではフロントエンドからきたGET/POSTリクエストを処理します.
GETリクエストの場合,フロントエンドは「http://~:3000/zukan:username」というように
自身のusernameをURLに付けてリクエストします.
サーバは送られてきたusernameを用いて図鑑用DBを検索し,usernameと一致したドキュメントを抽出します.
そして,ユーザが登録していた全キャラクターIDを一次元配列に格納し,フロントエンドに返します.
var express = require('express');
var bodyParser = require('body-parser');
var router = express.Router();
var mongoose = require('mongoose');
// モデルの宣言
var Zukan = require('../../models/zukan');
// GETリクエストがきた時の処理
/*
* Request URL: http://localhost:3000/zukan/:username
*/
router.get('/:username', function(request, response){
console.log("catch the get request for zukan");
response.setHeader('Content-Type', 'text/plain');
var username = request.params.username;
var characterID = {};
console.log(username);
Zukan.find({ "username" : username }, function(err, charId){
if (err)
console.log(err);
var cntID = 0;
for (var i=0; i < charId.length; i++){
for(var j=0; j < charId[i].characterID.length; j++){
characterID["id"+cntID] = charId[i].characterID[j];
console.log(characterID["id"+cntID]);
cntID++;
}
}
console.log(characterID);
response.json(characterID);
});
});
// 以下,POSTに対する記述が続く
.get()によってGETリクエストに対応します.
最初にURLをパースしてusernameを抽出します.
.find()によってusernameをキーに図鑑用DBを検索し,その結果を無名関数の第二引数charIDに格納します.
登録されているキャラクターIDをすべて抽出し,一次元配列characterID[]に格納します.
すべて格納し終わったら,フロントエンドにjson形式で返します.
以上がzukan.jsのGETリクエストに対する処理です.
次に,POSTリクエストに対する処理を記述していきます.
POSTリクエストの場合,さきほども述べたように
フロントエンドからキャラクターに関するパラメータが入ったjsonがサーバに渡されます.
(詳しいjsonの中身はご当地キャラカタログの「位置情報からキャラクターを検索」の
ところを見てください.)
フロントエンドから渡されたjsonをパースして,キャラクターIDを取り出します.
次に,usernameで図鑑用DB内を検索し,ユーザが登録されていない場合は,
usernameと取り出したキャラクターIDを用いて新規登録を行います.
ユーザがすでに登録されている場合は,ドキュメント内のキャラクターIDが登録されている配列に
取り出したキャラクターIDを追加して登録します.
// GETに対する処理(割愛)
// POSTリクエストがきた時の処理
/* POST内容はJSON形式で飛ばされる
* JSON format
* {
* "username" : "String"
* "characterID" : "String"
* }
*
* Request URL: http://localhost:3000/zukan
*/
router.post('/:username', function(request, response){
console.log("catch the post request for adding character ID");
response.setHeader('Content-Type', 'text/plain');
var username = request.params.username;
Zukan.find({ "username" : username }, function(err, result){
if (err)
console.log(err);
// パラメータ名、usernameとキャラID
var jsonarray = request.body["result"];
var idarray = [];
for (var i=0; i < jsonarray.length; i++){
idarray.push(jsonarray[i].id);
}
// 新規登録
if (result.length == 0){
var zukans = new Zukan();
zukans.username = username;
zukans.characterID = idarray;
console.log(zukans.username);
console.log(zukans.characterID);
// ユーザ情報を登録する
zukans.save(function(err) {
if (err) console.log(err);
});
}
// 追加登録
else{
result.map(function(doc){
var obj = doc.toObject();
for (var i=0; i < idarray.length; i++){
obj.characterID.push(idarray[i]);
}
Zukan.update({ "username" : username },
{ $set: { characterID: obj.characterID} },
function(err){
if (err) console.log(err);
Zukan.find({ "username" : username }, function(err, result){
if (err)
console.log(err);
});
});
});
}
});
});
module.exports = router;
ポイントは追加登録のところです.スキーマでcharacterIDを一次元配列として定義したため,
一度ドキュメントをtoObject()で取り出し,そこにリクエスト内容のキャラクターIDをpushしています.
そのあとはupdateをかけてもう一度find()で登録できたかどうかを確認しています.
ドキュメント内の一次元配列にどうやって追加するのかでかなり悩んだのですが,
以下のサイトに答えがありました.
Mongoose の Document インスタンスにプロパティを追加しての JSON 化 - とりあえず備忘録としてはじめようかな?
いやもうほんとこの部分でかなり苦労したのに,
あっさり解決しててハッカソン中大声あげてsugeeeeeってなってました.
周りのチームの方すみませんでした.
以上でzukan.jsの実装は終了です.
おわりに
今回は自身の備忘録として書いたので,読みにくい感じになってしまいました.
結局何書いてあるのかわからん状態になっているので,今後もう少し整理していこうかなと思います.
ともあれ,やっぱりハッカソンとかに参加しないと新しいことに挑戦する機会がないですね.
次回はどんな新しいことをやろうか考え中なので,こんな技術おもしろいよってことがあったら連絡くださいお願いします.
今回作成したプログラムは以下に置いてあるので,手っ取り早く動かしたい人はどうぞ.
https://github.com/tdomen/node.js_mongodb_server