出典について
この記事はcraigrussell氏によるMySQL High Availabilityの投稿「The JavaScript Connector for MySQL you never heard of」(2015/7/14)をユーザが翻訳したものであり、Oracle公式の文書ではありません。
Webサーバをたてて、node.jsとExpressを使ってデータベースに接続するのはかつてないほど簡単になっている。node.jsを使ってRDBからデータを取り出すには、かつてはユーザーがSQLの技術に熟練している必要があった。
そこでMySQLの開発チームは、MySQLからのドキュメントを保存、検索し、node.jsのアプリケーションに渡すのを何でもなくする(うまく、簡単にする)プロジェクトを立ち上げた。SQLを使わずにだ。この成果はmysql-jsと呼ばれ、ここでご紹介したいと思う。今日、githubでダウンロードできる。
プロジェクトは比較的シンプルなJSONのドキュメントを保存するところから検討を始めた。ドキュメントの各属性はデータベースのカラムに格納される。このモデルはシンプルなドキュメントをリレーショナルデータモデルの利点を最大限活用できるように保存できるようになっている。しかし、複雑な属性を保存するのは、簡単なことではなかった。
そこで、我々はある技術を開発した。この技術は、複雑な属性(例えば、配列、ネストされたJavaScripのオブジェクトなど)を保存される前にJSONにシリアライズする。データベースから属性値を検索する時は、シリアライズの処理が逆方向に行われ、元の属性が復元される。
この変更によって、構造がうまく定義されている均一なドキュメントを保存することが出来るようになった。しかし、追加の属性を持つドキュメントを保存するテーブルは依然として問題がたくさんあった。そこで全ての属性に対し、データベース内にカラムがない場合に保存するカラムを導入した。
このデータモデルが導入とともに、我々はデータアクセスAPIに専念した。基本的なこと、つまりinsertとdeleteからまず始めた。テーブルは主キーのカラムを持っており、ドキュメント内に対応する属性を持っていたため、ドキュメントを保存するのはどのテーブルにそのデータが属するかを判別し書込むのと同様、簡単であった。
ドキュメントの削除は簡単である。主キーもしくはユニークキーで削除したいドキュメントを識別すれば良い。
一度保存したドキュメントを検索するには、ドキュメントを主キーかユニークキーで判別できる必要がある。複数のドキュメントを返す任意の属性でのクエリは、明らかにより複雑であるためこれは別の問題として扱った。
ドキュメントの更新には2つのことが必要である。主キーまたはユニークキーを使ってどのドキュメントを更新すべきかを特定し、どの属性を変えるべきかを特定する。
コネクタへの主要なユーザーインターフェースはSession
である。MySQLのアーキテクチャにおいては、セッションはサーバ側のものでユーザーの代わりにデータにアクセスし、リクエストを受付け、結果を届ける役割がある。JavaScriptコネクターでは、ローカルセッションの中でリクエストを生成し、コネクタはサーバーセッションに転送する。
コードの例
セッションを取得するのは分かりやすい。MySQLサーバの接続情報を知っていれば、JavaScriptのオブジェクトが作成でき、セッションサービスにセッションを使うよう要請すればよい。サーバーが標準のホストおよびポートを利用していれば、コネクタが既知のデフォルトを利用できる。クレデンシャルを追加したければ、接続情報に追加して欲しい。
var mySQLClient = require('mysql-js');
var properties = new mySQLClient.ConnectionProperties('mysql');
properties.user = 'user1';
properties.password = 'sekrit';
mySQLClient.openSession(properties, onOpenSession);
セッションはコールバック内で受け渡される。コールバックはnode.jsの慣例に従い、2パラメータを持つ。errとdataである。このケースでは、dataはデータベースとやりとりをするのに使うセッションオブジェクトである。コネクタはPromisesやA+もサポートする(後ほど詳細を記述する)
CRUD操作
Session
はCRUD操作、クエリおよぼいいくつかのユーティリティー関数をサポートしている。CRUD操作では、データベース内の1行と、JavaScriptの1つのオブジェクトを扱う。オブジェクトはJSONのリテラル記法('{name: value}'の記法で直接作成されたもの)かコンストラクタで生成可能で、オブジェクト指向プログラミングの恩恵にあずかることができるだろう。
次に示すいくつかの例では、標準のデータベースにあるテーブルがあると仮定して欲しい。
CREATE TABLE author (
user_name varchar(20) NOT NULL PRIMARY KEY,
full_name varchar(250),
posts int unsigned not null default 0
) ENGINE=ndbcluster;
Insert
authorテーブルに行を挿入するには、insert
関数を使う。
var craig = {'user_name': 'clr', 'full_name': 'Craig Russell'};
session.insert('author', craig, onInsert);
posts列はデフォルト値を持っているので、挿入するデータに含まれていなくても良い。
Insert or Update
行をauthorテーブルに挿入、もしくはすでに存在すれば更新するには、save
関数を使う。これはSQLのON DUPLICATE KEY UPDATE
に相当する。(いくつかのAPIでは"write"という単語を使っているが、これにはそんなに意味内容がない。他のAPIでは"upsert"とよんでいるが、この言葉は混乱を呼ぶものと考えている。われわれは"indate"と考えたがこれでは役にたたないようである。)
var craig = {'user_name': 'clr', 'full_name': 'Craig Russell', posts:100};
session.save('author', craig, onSave);
Find
データベース内の単一行を検索するには、find
関数を使う。キーは完全な主キーであるプリミティブ、または主キーを属性として持つオブジェクト、またはユニークキーを属性として持つオブジェクトである。
function onFound(err, row) {
// error handling omitted for clarity
// prints {'user_name': 'clr', 'full_name': 'Craig Russell' 'posts': 100}
console.log(row);
}
// find(tableName, key, callback)
session.find('author', 'clr', onFound);
Update
データベース内の単一行を更新するには、update
関数を使う。キーは行をユニークに特定する為だけに利用され、値は属性を指定された値に更新する為だけに使われる。
// update(tableName, key, value, callback)
session.update('author', 'clr', {'full_name': 'Craig L Russell'}, onUpdate);
Delete
データベース内の単一行を削除する為には、delete
関数を使う。(t関数に別名をつけremove
関数にエイリアスした。IDEがdelete
の語句を予測しないコンテキストに推測するのが気にくわない場合に対応する為だ。)(訳注: エイリアスはdelete関数 > remove関数の誤りか。原文を合わせて参照ください)
// delete(tableName, key, callback)
session.delete('author', 'clr', onDelete);
コンストラクタの利用
アプリケーションをよりよく作り込む為には、コンストラクタを使いたくなるかもしれない。我々はMartin Fowlerのすばらしいリファレンスにあるドメインモデルのパターン、パターンオブエンタープライズアプリケーションアーキテクチャをサポートしている。このケースでは、セッションの操作の中でコンストラクタを定義してそれ(もしくはインスタンスを)使えばよい。
function Author(name, full_name) {
if (name) this.user_name = name;
if (full_name) this.full_name = full_name;
}
Author.prototype.getNumberOfPosts = function() {
return this.posts;
}
Author.prototype.toString = function() {
return ((this.posts > 100)?'Esteemed ':'') +
'Author: ' + this.name +
' Full Name: ' + this.full_name +
' posts: ' + this.posts;
}
コンストラクタを利用する際に1つだけ追加でしなければならないことがある。現在、コンストラクタはデータを保存するのに使うテーブル名の注記が付与されている。テーブル名が標準のコンストラクタの関数名となるよう改良中である。
new mySQLClient.TableMapping('author').applyToClass(Author);
Insert
authorテーブルに行を挿入するには、insert
関数を使う。コンストラクタを使っているので、テーブル名を指定する必要はない。コネクタはTableMapping
のテーブルを利用するだろう。
var craig = new Author('clr', 'Craig Russell';
session.insert(craig, onInsert);
posts列は標準の値を持っているので、挿入データに含まれる必要はない。
Insert or Update
authorテーブルに行を挿入するか、すでに存在する場合は更新するには、save
関数を使う。
var craig = new Author('clr', 'Craig Russell');
craig.posts = 100;
session.save('author', craig, onSave);
Find
データベース内の単一行を検索するには、find
関数を使う。
function onFound(err, row) {
// prints Author: clr Full Name: Craig Russell posts: 0
console.log(row);
}
session.find(Author, 'clr', onFound);
Update
データベース内の単一行を更新するには、update
関数を使う。
var key = new Author('clr');
var changes = new Author('', 'Craig L. Russell');
session.update(Author, key, changes, onUpdate);
Delete
データベース内の単一行を削除するには、delete
(もしくはremove
)関数を使う。
session.delete(Author, 'clr', onDelete);
Promise
コールバックとJavaScriptを使っている場合(言い換えれば、node.jsを通常通り使っている場合)、エラーハンドリングのコードは、アプケーションのコードを分かりづらくしうる。Promiseはエラーハンドリングが抽象化されており、よりきれいなコードを書く1つの手段である。
例えば、エラーハンドリングのコードは下記のようになるだろう。
// find an object
function onSession(err, s) {
session = s;
if (err) {
console.log('Error onSession:', err);
process.exit(0);
} else {
session.find('Author', 'clr', onFindByTableName);
}
};
実際のコードは1行しかないが、エラーハンドリングの中にうずもれている。
Promiseを利用すれば、その代わりに次のように書くことができる。
それぞれの関数はthen
関数から呼び出され連続して実行されるだろう。
var session;
function exit(code) {
process.exit(code);
}
function setSession(s) {
session = s;
}
function insertByConstructor() {
return session.insert(Author, new Author('sam', 'Sammy Snead'));
}
function insertByTableName() {
return session.insert('author',
{user_name: 'char', full_name: 'Charlene LeMain'});
}
function saveByConstructor() {
return session.save(Author, new Author('sam', 'Sammy Snead'));
}
function saveByTableName() {
return session.save('author',
{user_name: 'char', full_name: 'Charlene LeMain'});
}
function findByConstructor() {
return session.find(Author, 'sam');
}
function findByTableName() {
return session.find('author', 'char');
}
function updateByConstructor() {
return session.update(Author, 'sam',
new Author(null, 'Samuel K Snead'));
}
function updateByTableName() {
return session.update('author', 'char',
{full_name: 'Charlene K LeMain'});
}
function deleteByConstructor(sam) {
return session.delete(Author, 'sam');
}
function deleteByTableName(char) {
return session.delete('author', 'char');
}
function closeSession() {
return session.close();
}
function reportSuccess() {
session.sessionFactory.close();
console.log('All done.');
exit(0);
}
function reportError(e) {
console.log('error:', e);
exit(1);
}
mySQLClient.openSession(dbProperties, null)
.then(setSession)
.then(insertByTableName)
.then(insertByConstructor)
.then(saveByTableName)
.then(saveByConstructor)
.then(updateByTableName)
.then(updateByConstructor)
.then(findByTableName)
.then(deleteByTableName)
.then(findByConstructor)
.then(deleteByConstructor)
.then(closeSession)
.then(reportSuccess, reportError);
Promiseは2つの引数を持つthen
メソッドの実装(mysql-js connector)定義を必要とする。すなわち非同期の操作が成功した後、単一の値を返す関数と、非同期の操作が失敗した時だけに呼び出され、例外を発生させる関数である。アプリケーションはthen
関数を利用でき、エラーハンドリングを含め、様々な非同期の関数を作れる。
ディスカッションすべきトピックは、まだまだたくさんある。ACID属性が保証されたトランザクション、複数のバックエンドストレージ、複雑なクエリそして複雑なドキュメントストレージの利用および扱い、ドキュメント指向のテーブルの正規化されたリレーショナルテーブルとの結合など。しばらくお待ちいただきたい。