Netflix Falcorについて(1)
Netflix Falcorについて(2)
Netflix Falcorについて(3)
Netflix Falcorについて(4)
Netflix Falcorについて(5)
今回は、JSON Graphの具体例とCALLメソッドについてです。
まずはJSON Graphについて説明します。
https://netflix.github.io/falcor/documentation/jsongraph.html
今まで見てきたことをも少し厳密に振り返りましょう。
クライアント -- Model -- DataSource -- MongoDBなど
※コーディングとしてはクライアントとDataSourceの実装が必要になります。
クライアントはModelのメソッドを通して、DataSourceからJSON Graphの一部を値として取得します。ModelメソッドにはJSON Graphから値を取得するだけのget()と、JSON Graphの一部を書き換えるcall()があります。call()は、REST APIにおけるPOSTに相当し、DBへの追加や更新・削除を行う場合に利用されます。
DataSourceにもget()とcall()メソッドが存在し、それぞれModelのメソッドに対応しています。
Modelのget()はDataSourceのget()を利用しているわけですが、ModelとDataSourceのreturn値のtypeにはちょっとした違いがあります。ちなみにreturn値はJSONGraphの一部であり、envelopと呼ばれています。
Modelのget() やcall()のreturn値のtypeはJSONEnvelopeです。
model.get(["todos", {from: 0, to:1},"name"], ["todos", "length"]).then(function(response){
console.log(JSON.stringify(response));
});
// The following JSON envelope is eventually printed to the console:
// {
// json: {
// todos: {
// "0": {
// name: 'get milk from corner store'
// },
// "1": {
// name: 'withdraw money from ATM'
// },
// "length": 10
// }
// }
// }
//★キーがjsonです
他方DataSourceのget() やcall()のreturn値のtypeはJSONGraphEnvelopeです。
router.get(["user", "name"]).subscribe(function(jsonGraphEnvelope) {
console.log(JSON.stringify(jsonGraphEnvelope, null, 4));
});
// the result of a get operation for the Path ["user", "name"]
// {
// jsonGraph: {
// user: {
// name: "Anupa"
// }
// }
// }
//★キーがjsonGraphです
さてあらためてですが、JSON Graph はグラフ情報をJSON objectを使って表現する方法です。Falcorはアプリーケーションで扱う全データをsingle JSON Graph objectとして表現します。JSONでグラフを表現するために "ref" typeが導入されます。
{ $type: "ref", value: ["todosById", 44] }
これは ["todosById", 44] はJSON Graph のパスであり、リファレンス(UNIX ファイルシステムのシンボリックリンクのようなもの)であるという意味です。この実体にアクセスしたい場合は、パス["todosById", 44] を辿りなさいという事です。
{
todosById: {
"44": {
name: "東武動物公園のプールに行く",
done: false,
prerequisites: [{ $type: "ref", value: ["todosById", 54] }]
},
"54": {
name: "水着を買いに行く",
done: false,
prerequisites: []
},
"64": {
name: "Netflixでシューターを観る",
done: false,
prerequisites: []
}
},
todos: [
{ $type: "ref", value: ["todosById", 44] },
{ $type: "ref", value: ["todosById", 54] },
{ $type: "ref", value: ["todosById", 64] }
],
summer: [
{ $type: "ref", value: ["todosById", 44] },
{ $type: "ref", value: ["todosById", 54] }
]
};
この例で説明すると、todos[]とsummer[]は2つの実体へのシンボリックリンクを共有していますが、実体はtodoByIDプロパティに存在し、それぞれ1つづつしか存在しません。各リストで実体のコピーを持つ必要がなくなり、メモリ容量や通信量の節約になるとともに、データの一貫性に寄与します。Netflixの動画リストで言えば、最新リストや人気リスト、お勧めリストなどリストは複数持つけれども、それらは単なるシンボリックリンクの配列であり、ドラマの詳細オブジェクトは1個だけ保持すればよくなります。
次に実際の動作するプログラムを例に、callメソッドとJSONGraphの説明を行いたいと思います。
mkdir falcor-qiita4
cd falcor-qiita4
npm init
npm install express --save
npm install falcor-express --save
npm install falcor-router --save
npm install mongoose --save
npm install body-parser --save
npm install falcor-json-graph --save
mongoimport --db qiita4db --collection articles --jsonArray initData.js --host=127.0.0.1
node index.js
DBの初期データとして2個のcollectionを作ります。
[
{
title: '夏休み1日目',
content: '1日目は東武動物公園のプールに行った'
},
{
title: '夏休み2日目',
content: '2日目は森林公園に行ってセミをとった'
}
]
今回のプログラムも、今までと同じようにクライアント側(index.html)に余分なロジックは持たせません。ただひたすら、何も考えずに、シーケンシャルにmodelのapiを叩いていくだけです。ただ適切なパスでVirtual JSONのデータにアクセスします。
まず最初に以下の手順で確認を加えながら新articleをaddします。
①add前のlengthを取得
-- 初期データの2がgetできます。
②新articleのadd、新_id(newArticleID)をgetする
-- call()で新データをaddします。returnとして新_id(newArticleID)を得ます
③addした_idをもとに新articleの実体をget
-- 新_id(newArticleID)をもとに、実体をgetします。
④add後のlengthを取得
-- add後のlengthの3が得られます。
次に以下の手順で今追加したデータの削除を行います。
⑤delete前のlengthを取得
-- ④と同じ3が得られます。
⑥delete実行
-- call()で新データ削除します。キーとして新_id(newArticleID)を与えます。
⑦delete後にarticle実体にアクセスできないことを確認
-- 本当に削除されたか、キャッシュも残っていないかを③と同じget()で確認します。 undefinedでok
⑧delete後のlengthを取得
-- ⑤の結果らから-1の2となります。
<html>
<head>
<!-- Do _not_ rely on this URL in production. Use only during development. -->
<script src="//netflix.github.io/falcor/build/falcor.browser.js"></script>
<script>
var model = new falcor.Model({source: new falcor.HttpDataSource('/model.json') });
async function fetch() {
//---------------add articles
// ①add前のlengthを取得
await model.
get("articles.length").
then(function(response) {
var res = JSON.stringify(response,null,2);
console.log(res);
document.write("<div>addの前"+res+"</div>");
});
// ②新articleのadd、新_id(newArticleID)をgetする
var newArticle = {
title: "テスト1",
content: "これはテスト1のコンテンツです"
};
var newArticleID = await model.
call( 'articles.add',[newArticle] ).then((result) => {
var res2 = JSON.stringify(result,null,2);
console.log(res2);
document.write("<div>"+res2+"</div>");
return model.getValue(
['articles', 'newArticleID']
).then((articleID) => {
return articleID;
});
});
document.write("<div>"+newArticleID+"</div>");
// ③addした_idをもとに新articleの実体をget
await model.
get(['articlesById',[newArticleID],['_id','title', 'content']]).
then(function(articlesResponse) {
var res2 = JSON.stringify(articlesResponse,null,2);
console.log(res2);
document.write("<div>"+res2+"</div>");
});
// ④add後のlengthを取得
await model.
get("articles.length").
then(function(response) {
var res = JSON.stringify(response,null,2);
console.log(res);
document.write("<div>addの後"+res+"</div><br/>");
});
//---------------delete articles
// ⑤delete前のlengthを取得
await model.
get("articles.length").
then(function(response) {
var res = JSON.stringify(response,null,2);
console.log(res);
document.write("<div>deleteの前"+res+"</div>");
});
// ⑥delete実行
let deletetionResults = await model.call(['articles', 'delete'],[newArticleID])
.then((result) => {
return result;
});
var res2 = JSON.stringify(deletetionResults,null,2);
console.log(res2);
document.write("<div>"+res2+"</div>");
// ⑦delete後にarticle実体にアクセスできないことを確認
// cacheを削除しないと、deleteしたarticleにアクセスできてしまう
await model.
get(['articlesById',[newArticleID],['_id','title', 'content']]).
then(function(articlesResponse) {
var res2 = JSON.stringify(articlesResponse,null,2);
console.log(res2); // undefinedを出力
document.write("<div>"+res2+"</div>");
});
// ⑧delete後のlengthを取得
await model.
get("articles.length").
then(function(response) {
var res = JSON.stringify(response,null,2);
console.log(res);
document.write("<div>deleteの後"+res+"</div><br />");
});
}
fetch();
</script>
</head>
<body>
</body>
</html>
次にサーバ側のDataSource層の実装です。前回のパッケージに加えて以下の2つの追加が必要となります
body-parserはexpressがPOST処理に対応するのに必要です。bodyのパースを行います。
falcor-json-graphによってref関数を使え、ref typeオブジェクトを短い表現に置き換えることができます。例えばjsonGraph.ref("todos[0].name")は { $type: "ref", value: ["todos", 0, "name"] }に等しいものです。
このサーバのプログラムで、クライアントからの要求に応じて、MongoDBのデータをもとに、JSON Graphを創り出しreturnしています。
「route: 'articles.add'」のハンドラでは、MongoDBへの追加の後に、3個のオブジェクトをresultsとしてreturnしています。call()メソッドの実行でJSON Graphに副作用が起こり、そのせいで生じたModelのキャッシュの整合性の問題点がが、このresultsで正されることになります。
またdeleteにおいては、invalidated: true、とすることでキャッシュの古いパスの値を明示的に削除しています。
{
path: ['articlesById', deleteId], // このarticle実体は副作用を受ける(削除される)
invalidated: true // これでcacheを消す:: falcor-routerの古いverだとinvalidate !?
},
var falcorExpress = require('falcor-express');
var Router = require('falcor-router');
var jsonGraph = require('falcor-json-graph');
var $ref = jsonGraph.ref;
var express = require('express');
var app = express();
var bodyParser = require('body-parser'); // call(POST)メソッド処理のために必要
app.use(bodyParser.urlencoded({extended: false}));
//---------- 以下はMongoDB & Mongooseのお決まりの設定です
var mongoose = require('mongoose');
var databaseUri = 'mongodb://localhost/qiita4db';
mongoose.Promise = global.Promise;
mongoose.connect(databaseUri, { useMongoClient: true })
const articleSchema = {
title:String,
content:String
};
const Article = mongoose.model('Article', articleSchema, 'articles');
//----------
app.use('/model.json', falcorExpress.dataSourceRoute(function (req, res) {
return new Router([
//================== model.get("articles.length")
{
route: 'articles.length',
get: () => { return Article.count({}, (err, count) => count)
.then ((articlesCount) => {
return {
path: ['articles', 'length'],
value: articlesCount
};
})
}
},
//================== model.call( 'articles.add',[newArticle] )
{
route: 'articles.add',
call: (callPath, args) => { //callの場合はargsにクライアント側の引数[newArticle]が渡される
var newArticleObj = args[0];
var article = new Article(newArticleObj);
return article.save(function (err, data) {
if (err) {
console.info("ERROR", err);
return err;
}
else {
return data;
}
}).then ((data) => {
return Article.count({}, function(err, count) {
}).then((count) => {
return { count, data };
});
}).then ((res) => {
var newArticleDetail = res.data.toObject();
var newArticleID = String(newArticleDetail["_id"]);
var NewArticleRef = $ref(['articlesById', newArticleID]); // 実体へのシンボリックリンク
// addによってJSON Graphは副作用を受けます。resultsによって結果を返すだけでなくキャッシュを更新します
var results = [
{
path: ['articles', res.count-1], // JSON Graphのarticles配列の最後に新article(の$ref)を追加
value: NewArticleRef
},
{
path: ['articles', 'newArticleID'], // addの結果として追加されたarticleの_idを返す
value: newArticleID
},
{
path: ['articles', 'length'], // addの影響を反映させるためcacheのlengthを更新する
value: res.count
}
];
return results;
});
}
},
//================== model.get(['articlesById',[newArticleID],['_id','title', 'content']])
// articlesByIdがarticle実体の定義になります
{
route: 'articlesById[{keys}]["_id","title","content"]',
get: function(pathSet) {
var articlesIDs = pathSet[1];
return Article.find({ // ハンドラはMongoose Promiseを返す
'_id': { $in: articlesIDs} // DBへの1回のアクセスで必要な全てを配列としてget
}, function(err, articlesDocs) {
return articlesDocs;
}).then ((articlesArray) => {
var results = [];
articlesArray.map((articleObject) => { // getした配列をmapで処理
var articleResObj = articleObject.toObject(); // Mongoose Object を normal Objectに変換
var idString = String(articleResObj['_id']);
results.push({
path: ['articlesById', idString],
value: articleResObj
});
});
return results;
});
}
},
//================== model.call(['articles', 'delete'],[newArticleID])
{
route: 'articles.delete',
call: (callPath, args) => {
var deleteId = args[0];
return Article.find({ _id: deleteId }).remove((err) => {
if (err) {
console.info("ERROR", err);
return err;
}
}).then((res) => {
// deleteによってJSON Graphは副作用を受けます。resultsによって結果を返すだけでなくキャッシュを更新します
return [
{
path: ['articlesById', deleteId], // このarticle実体は副作用を受ける(削除される)
invalidated: true // これでcacheを消す:: falcor-routerの古いverだとinvalidate !?
},
{
path: ['articles', 'length'], // artcles配列の長さも副作用を受ける
invalidated: true // これでcacheを消す
},
{
path: ['articles', 'deletedArticleID'], // クライアントが必要とすれば削除結果として返す
value: deleteId
},
]
}); //then
}//call
}
]);
}));
// serve static files from current directory
app.use(express.static(__dirname + '/'));
var server = app.listen(3000);
最後に、今回の環境のpackage.jsonを挙げておきます。
{
"name": "falcor-qiita4",
"version": "1.0.0",
"description": "mkdir falcor-qiita3\r cd falcor-qiita3\r npm init",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.17.2",
"express": "^4.15.3",
"falcor-express": "^0.1.4",
"falcor-json-graph": "^1.1.7",
"falcor-router": "^0.8.1",
"mongoose": "^4.11.5"
}
}