ドキュメント指向DBとそこからMySQLが取り入れたものを考える

 ドキュメント指向DBは任意のドキュメントの形でデータを扱うDBだ。MongoDBならそのドキュメントは基本的にJSONである。JSONの形でデータを扱ってなにがうれしいかというと、一件のデータをスキーマレスな形で扱えるということである。これはRDBでいうところの、一つのテーブルに、バラバラなカラムを持ったデータをつっこめるということだ。でもそんなのは極端な話で、もっと簡単なところでは、カラムの型としてリストが持てる上に、それが容易に検索に使えるところがイカしてると思っている。

 これまでのRDBではリストのような、長さの決まっていない型を持つのは難しかった。そういうことができるように工夫されたRDBもあるにはあったが。ないゆえに一対多や多対多をやるには中間テーブルを組んだりするのが流儀だった。
 たとえばこのブログの記事を従来の方法でRDBに保存することを考える。タイトル、日付、本文は文字列や日付のような型でカラムを作って、一つのレコードに保存できる。タグは新たにテーブルを作って、タグの数だけレコードが従来のまっとうなやり方だろう。JSONでそれらをそのまま表現してみると下のようになるだろう。タグは三つ作るとして。
{

"_id": 1,
"title": "タイトル",
"date": "2017-5-11 10:12:123.11111",
"body": "本文だよーん"
}

{

"ref": 1,
"tag": "タグ1"
}
{
"ref": 1,
"tag": "タグ2"
}
{
"ref": 1,
"tag": "タグ3"
}


 一件のブログ記事を表現するのに、二つのテーブルにまたがって、なおかつ二つ目のテーブルでは三件のレコードを必要としている。タグの一致する記事を探したければ、一致タグを探してテーブル結合して、という手順を踏む必要がある。
 ではでは。ドキュメント指向DBのMongoDBで一件の記事の保存を考えてみる。下記のようになる。
{

"_id": 1,
"title": "タイトル",
"date": "2017-5-11 10:12:123.11111",
"body": "本文だよーん",
"tags": ["タグ1", "タグ2", "タグ3"]
}

 一件のブログ記事が一件のドキュメント(RDBでいうところのレコード)にまとまる。シンプル。
 たとえば"タグ1"を持っている記事を検索したいってのはどうするのかというと下記のようになる。
db.entries.find({"tags": "タグ1"});

一件のドキュメントにタグまでまとまっているので結合はしない。シンプル。
 MognoDBは結合をもっていない(はず)なので、DB側でリレーションを組むことはできない。まあDB側でJSを動かせるのでやりようはあるが、今回のようなケースではなくても十分足りている。
 データをそれなり正規化して、各テーブルを用意して関係を組んでいくというところではRDBはベストなソリューションだろう。一方で、データをシンプルな表現で保存したり検索したい、関係を組んだりはしないよ、という要求があった場合にはMongoDBのような形式のドキュメント指向DBはかなりいいソリューションだと思う。DBとデータのやり取りをするとき、シンプルな形で扱うことができるから。

 JSONでデータを表現できるのは、場合によってはとてもクールなことだと思う。ここで書いた例では、ブログ記事一件を一件のデータとして、シンプルな構造で扱えた。
 たぶんそんな機能がイカしてるとMySQL開発の面々も感じたんだろう。MySQLでもカラムの型としてJSONがサポートされるようになって少し経っている。これによって、MongoDBで示した例と似たようなことができるようになっているのは前のブログで書いた。
MySQLのデータ型にJSON型が加わっていた

 趣味でやっているところでは関係を組むとかあまり必要ではないので、趣味で使うDBのデフォはMongoDBになっている。仕事では全部をMongoDBにしちゃうなんてことは、機能を検討するとそうもいかないことは結構ある。それでもシンプルなWebアプリケーションの構築もぼちぼち来る。そういったときはドキュメント指向DB、あるいはMySQLでJSON型のカラムを使うことも考えていきたい。MongoDBは銀の弾丸ではないが、かといって従来のRDBにもそれは言えることだと思っているがはてさて。
comment: 0

C#: MongoDBでのCRUD操作

C#でMongoDBのORマッパ
 前回に、C#でMongoDBを使うためのORマッパとなるクラスを書いた。今回はそのクラスを使ったり使わなかったりでデータのCRUD操作。
public class Entry

{
[BsonId]
[BsonElement("publishDate")]
[BsonRepresentation(BsonType.Int32)]
public int? EntryID { get; set; } = null;

[DisplayName("タイトル")]
[BsonRepresentation(BsonType.String)]
[BsonElement("title")]
[BsonRequired]
public string Title { get; set; }

[DisplayName("発行日時")]
[BsonRepresentation(BsonType.DateTime)]
[BsonElement("publishDate")]
[BsonRequired]
public DateTime PublishDate { get; set; }

[DisplayName("タグ")]
[BsonRepresentation(BsonType.String)]
[BsonElement("tag")]
public List<string> Tag { get; set; }

[DisplayName("本文")]
[BsonRepresentation(BsonType.String)]
[BsonRequired]
[BsonElement("body")]
public string Body { get; set; }
}



・Create
public bool Insert ()

{
var collection = DbConnection.db.GetCollection<Entry>("entry");
collection.InsertOne(this);

return true;
}



・Read
-IDで絞って読む
public static Entry GetEntry(int id, bool admin=false)

{
var collection = DbConnection.db.GetCollection<Entry>("entry");

FilterDefinition<Entry> filter;
var f = Builders<Entry>.Filter;
filter = f.Eq(e => e.EntryID, id);

var entry = collection.Find<Entry>(filter).FirstOrDefault();

return entry;
}


-日付の新しい順で任意件数読む。スキップもできるように
public static List<Entry> GetRecentEntry(int skip_n, bool admin=false, bool isSitemap=false) {

var collection = DbConnection.db.GetCollection<Entry>("entry");
var skip = skip_n * LIMIT;

FilterDefinition<Entry> filter;
filter = new BsonDocument { };

var sortDoc = new BsonDocument
{
{ "publishDate", -1 },
};

var entries = collection.Find<Entry>(filter).Sort(sortDoc).Limit(LIMIT).Skip(skip).ToList();

return entries;
}


-IDが違って、同じタグのあるEntryを読み込む
public static List<Entry> GetSameTagEntry(List<string> filterTag, int skip_n, int? excludeId = null)

{
var collection = DbConnection.db.GetCollection<Entry>("entry");
var skip = skip_n * LIMIT;

FilterDefinition<Entry> filter;
var f = Builders<Entry>.Filter;
filter = f.Ne(e => e.EntryID, excludeId) &
f.AnyIn(e => e.Tag, filterTag);

var sortDoc = new BsonDocument
{
{ "publishDate", -1 },
};
var entries = collection.Find<Entry>(filter).Sort(sortDoc).Limit(LIMIT).Skip(skip).ToList();

return entries;
}


-任意の月に書かれたEntryを読む
public static List<Entry> FilterByMonth (int year, int month, int skip=0)

{
var collection = DbConnection.db.GetCollection<Entry>("entry");

var dateMin = new DateTime(year, month, 1);
var dateMax = dateMin.AddMonths(1);

FilterDefinition<Entry> filter;
var f = Builders<Entry>.Filter;
filter = f.Gte(e => e.PublishDate, dateMin) &
f.Lt(e => e.PublishDate, dateMax);

var sortDoc = new BsonDocument
{
{ "publishDate", -1 },
};
var entries = collection.Find<Entry>(filter).Sort(sortDoc).Limit(LIMIT).Skip(LIMIT * skip).ToList();

return entries;
}




・Update
public bool Update ()

{
var collection = DbConnection.db.GetCollection<BsonDocument>("entry");

var filter = Builders<BsonDocument>.Filter.Eq("_id", EntryID);
collection.ReplaceOne(filter, this.ToBsonDocument());

return true;
}



・Remove(複数まとめてできるように)
public static bool DeleteMany(List<int> ids)

{
var collection = DbConnection.db.GetCollection<BsonDocument>("entry");

var filter = Builders<BsonDocument>.Filter.In("_id", ids);
collection.DeleteMany(filter);

return true;
}
comment: 0

MongoDBのデータをC#のオブジェクトにマッピングして使う:ORマッパー

 このブログのデータベースはMongoDBを使っている。これまでデータベースから引っ張ってきたドキュメント(RDBでいうところの行データ)は、引っ張ってきた生の状態ではJSONなので、そこからC#の適切な型にキャストされるように手書きしていた。

 たとえば記事データは下記のようなプロパティを持ったオブジェクトで表していた。
public int? EntryID { get; set; } = null;


[DisplayName("タイトル")]
public string Title { get; set; }

[DisplayName("発行日時")]
public DateTime PublishDate { get; set; }

[DisplayName("本文")]
public string Body { get; set; }


 ↑のオブジェクトをJSONでサンプルを作ると下記。
{"EntryID":3, "Title":"今日の献立", "PublishDate":"2016 10:10:10",
"Tag":["夕食","料理"], "Body":"ほにゃらら"}

 で、↑のJSONをMongoDBからひっぱってきたときに、↑の↑にあるC#オブジェクトにキャストしていた。下記のように。
var doc = collection.Find<BsonDocument>(filter).FirstOrDefault();

if (doc == null)
{
return;
}

var entry = new Entry
{
Title = doc.GetValue("title").AsString,
Body = doc.GetValue("body").AsString,
EntryID = Convert.ToInt32(doc.GetValue("_id").ToDouble()),
PublishDate = doc.GetValue("publishDate").ToUniversalTime()
};

いちいちメソッド使ったりなんだで値をキャストしていくのが面倒だった。なのでORマッパを用意して下記のように書き換えた。一発。
var entry = collection.Find<Entry>(filter).FirstOrDefault();


 オブジェクトの定義は下記のようになった。
[BsonId]

[BsonElement("publishDate")]
[BsonRepresentation(BsonType.Int32)]
public int? EntryID { get; set; } = null;

[DisplayName("タイトル")]
[BsonRepresentation(BsonType.String)]
[BsonElement("title")]
[BsonRequired]
public string Title { get; set; }

[DisplayName("発行日時")]
[BsonRepresentation(BsonType.DateTime)]
[BsonElement("publishDate")]
[BsonRequired]
public DateTime PublishDate { get; set; }

[DisplayName("タグ")]
[BsonRepresentation(BsonType.String)]
[BsonElement("tag")]
public List<string> Tag { get; set; }

[DisplayName("本文")]
[BsonRepresentation(BsonType.String)]
[BsonRequired]
[BsonElement("body")]
public string Body { get; set; }

注意として、MongoDBがスキーマレスなのに反してC#は基本的に静的型付けであるから、C#側で未定義なキーがJSON側にあるとエラーでデータがマッピングできない。あと保存しないプロパティにはBsonIgnore属性をつける必要がある。



*以下ひとりごとメモ
ORマッパを使いたい
↑データ保存、読み出しでオブジェクト指向とのギャップを感じる
↑アプリケーションを書いている言語(非SQL)とは別の言語(SQL)が混ざってくる
↑行のデータを row['title'] のような表現で読みだしてるとか、ほんとうにデータベースを使ってる感が出てくる。
↑さらに、安全にやるにはプレースホルダを置いて、値をバインドしてということをシコシコとやる必要がある。値のバインドは非SQLな言語側での話。非SQLが安全なSQL構築の手助けをしてあげなきゃいけない。データを操作するためのSQLを構築するために非SQLでSQLを構築していかなければならない
↑結局コードがSQL、非SQLでごちゃごちゃしてくる
↑文字列型でSQL文を書いてそこにパラメータをバインドして、ってのがイケてない。セキュリティを確保しつつなんかもっと楽な手段はないか(LINQとかイケてると思う)。
↑データベースを使ってることをもうちょい隅のほうに追いやるとか隠ぺいとかできんか

データベース使って、そもそもなにをしたかったの?
→データの永続化だよ
→"オブジェクトを自由に冷凍保存して、自由に解凍しているような感覚" http://blog.jnito.com/entry/20100528/1274998429
→オブジェクト指向したいやつには最高じゃねーか
→ORマッパなしで書いていてオブジェクト指向との乖離を感じる。ORマッパを使っているサンプルコードを見ると、完全なのぞみは叶っていないにせよ、かなり好みのところまでいってる

 ORマッパの書き方を学ぶのがめんどうでベタベタで書いていたが、いろいろ書いていてベタベタなほうが面倒だと思うようになってきた。row['title'] のような辞書型系統での値の読出しや、ここに入っている値の非SQLのほうの型へのキャストをシコシコと書くのとおさらばしたかった。これからはORマッパさんとまじめにお付き合いしていこうと思う。
comment: 0

このブログのDBをMongoDB(mLab)からDocumentDBにしようと考えたけどやめたというメモ

 AzureのDocumentDBがMongoDBとの完全互換を得たというのを読んだ。
https://blogs.msdn.microsoft.com/visualstudio_jpn/2016/12/16/nosqlnetcoredevelopmentusinganlocalazuredocumentdbemulator/
 DocumentDBは日本のAzureサーバでも使える。
 このブログのDBはMongoDBを安く使いたいためにMongoDBのホスティングを行っているmLabを使っている。そんでもってmLabの共用のサーバは日本にないので、エリアをアメリカの西海岸で選択している。DBがアメリカ西海岸なので、アプリのサーバも近いほうがいい。だからアプリもアメリカ西海岸。
 DocumentDBに関してのを読んで、DocumentDBに乗り換えればDBサーバを日本に持ってこれるからアプリも日本に持ってこれる、と考えた。そしてDocumentDB、実際いくらぐらいで使えるねん、というのを調べた。

 最低単位の100RU(秒間で1kBを100件さばけるぐらいというのを示す単位)で一カ月607円ぐらい。いける、と思った。でも実際ここをもうちょっとつっこむとネックがあった。
 まず、最低のRUは400で、そこから100刻みで上げていけるようになっている。つまりDocumentDBを使いたいなら最低は2400円程度が月にかかることになる。しかもこれ、コレクション一つに対する課金である。コレクションを増やしたければ、1コレクションごとに最低400RUを設定する。このブログにはコレクションを3つ使っているので...月額8000円ちょっとである。広告置いてない。金稼いでるブログじゃない。無理。




 回避策がないわけではない。コレクションすべてを一つにまとめて、ドキュメントごとにもともと分類されていたコレクション名を値で持ってしまえばどうにかならんこともないだろう。でもコレクションをひとまとめにするなんて見苦しいことこの上ないのでできない。

 というわけでいつかまた会いましょう。その日までお元気でDocumentDB。未練たらたらで使いたくてたまらないのではあるが...
comment: 0

ASP.NET Core MVC: DockerでWebアプリのテスト用DBを用意してもろもろの設定

 このブログのバックエンドはASP.NET Core MVCで作っていて、これからもメンテナンスを続けていく。それなのに、.NET Coreのリリースが迫っているときに突貫で作ったために、CI(継続的インテグレーション)の準備ができていない。だからこれから整えていく。まずはテストを用意するため、テスト用のDBを用意するところから。

 一番手っ取り早いDBの準備方法は、開発マシンにDBをインストールしてしまうことだろう。だけどそれでは不要に開発環境が汚れて好みではない。仮想環境に用意してそこに接続したい。そこで今回はそういったことが簡易にできるDockerを使うことにする。DBはMongoDBで。

 ぼくの環境ではDocker Machineが入っているのでそれで適当な名前のDocker環境を用意して、そのDockerへSSH接続する。
docker-machine ssh dev


 Docker環境(コンテナホスト)に入ったらMongoDBが入ったイメージを落として、外部からコンテナの中で動いているMongoDBにアクセスできるようにポートをバインドする。
docker pull mongo

docker run --name bolog-test-mongo -p 80:27017 -d mongo



 Dockerの外部である開発環境からMongoDBへ接続をから接続を試す。今回はMongoDBに付属しているmongoを使う。


 あとはデータベースに必要なデータをつっこんでおく。

 データベースの準備ができたので、Visual Studioで作ってあるWebアプリのプロジェクトからアクセスできるように接続文字列などを設定する。

 まずテスト用DBの接続文字列設定。appsettings.jsonにMongoDB接続の文字列を追加。パスワードとか入っていないローカルネットワークオンリーのDBなので接続文字列が公開されてしまっても大丈夫。
{

"ConnectionStrings": {
"Mongo": "mongodb://192.168.11.5"
},
"Logging": {


 続いてAzureのほうに、本番用DBの接続文字列を入れる。アプリケーション設定のところに入れられる場所があるのでそこに。今回はCustomというカテゴリで、テスト用DBと同様にMongoという名前で値を入れた。


 アプリケーション内でのDB接続文字列の呼び出しは下記で行う。
using Microsoft.Extensions.Configuration;

// *******
Configuration.GetSection("ConnectionStrings")["Mongo"]


 これでテスト、本番で使用するDBを自動でわけつつ、本番用は一般には入れない場所に接続文字列を置くことができた。Githubからデプロイしたい場合、パブリックリポジトリにシークレットなデータを置くわけにはいかないのでこれを使えば回避できる。

 DBを用意したので引き続きCI構築としてテストを作っていきたい。
comment: 0