Morning Girl

Web API, Windows, C#, .NET, Dynamics 365/CRM etc..

CData MongoDB Driverを使ってドキュメント指向NoSQLであるMongoDBをRDBライクに扱う方法

本日も、またまたMongoDBです。今回は前回使用したCData MongoDB Driverの内部的なお話。

MongoDBのようなドキュメント指向NoSQLは、アプリケーションからプログラムライクに扱う時にはスムーズにアプローチできますが、最終的に分析したい、BIツールなどで咀嚼したい、といった時にはこのスキーマレスな階層構造が邪魔をしてしまいます。

そこを、前回の記事ではCData MongoDB JDBC Driverを挟むことで、RDBライクにアクセスし、BIツールで使いやすいフラットな状態にした上で扱いました。

kageura.hatenadiary.jp

www.cdata.com

今回は、CData Driverがどのようにドキュメント指向NoSQLをRDBライクに扱っているのか、どんなアプローチでMongoDBのデータを解釈可能なのかを見ていきたいと思います。

詳しくは、ヘルプにも掲載している内容ですので、併せて見てもらえるといいのかなと思います。

cdn.cdata.com

MongoDBをどのようにRDBライクに扱っているのか?

MongoDBをRDBライクに扱う上で、JSONライクなデータ(MongoDBではBSON)を2次元のテーブル構造としてどのように構成し直すかが、基本的なポイントとなります。

そして、課題点大きく取り上げるべき点は2つかなと個人的に考えています。

・ネストされたオブジェクト階層構造の解釈

・配列形式構造の解釈

f:id:sugimomoto:20180114125615p:plain

このボトルネックとなるその2つのポイントをどのように解釈し、調整しているのかを見ていきたいと思います。

ネストされたオブジェクトのアクセスパターン

結構これはシンプルだと思いますし、直感的でしょう。

例えば、以下のようなオブジェクトの場合

{
  id: 12,
  name: "Lohia Manufacturers Inc.",
  address: {street: "Main Street", city: "Chapel Hill", state: "NC"},
  offices: ["Chapel Hill", "London", "New York"],
  annual_revenue: 35,600,000
}

[address]配下の[street][city][state]がネストされたオブジェクトになります。

CData Driverでは、初期状態で[FlattenObjects]というプロパティがTrueに設定されていることにより、このネストされたオブジェクトを以下のようにフラットな表形式として咀嚼して、スキーマを検出します。

f:id:sugimomoto:20180114125800p:plain

では、[FlattenObjects]を無効(False)にした場合は、どうなるかと言えば、以下のようにJsonをそのままStringで格納した形で検出します。

f:id:sugimomoto:20180114125804p:plain

構造次第ではこれもありなのかもしれないですが、ケースバイケースでしょう。

なので、いくらオブジェクトがネストされていようが、フラットな形式でRDBライクに出力します。

ただし、間に配列構造が挟まれた場合はちょっと話が異なります。

配列構造のアクセスパターン

そもそもRDBで言うところの配列構造は、見積もりのヘッダと明細のように基本的にはテーブルを分けて、正規化するのが一般的なアプローチでしょう。しかしながら、ドキュメント型NoSQLであるために、正規化されずに1ドキュメントとして扱われるシチュエーションがあります。

MongoDBの場合、Joinのようなクエリがサポートされていないことも、データの構造としてのネストの深さに拍車をかけているかもしれません。

それをCData Driverでは大きく2種類のパターンで、配列構造をRDBライクにアクセスできるように調整しています。

一つは、GIS系でよく利用する以下のような緯度経度のような配列構造の上限がある程度存在するタイプのものを解釈する場合です。

"coord": [ -73.856077, 40.848447 ]

これはFlattenArraysというプロパティを2にすることで、以下のようにフラットな形で扱うことが可能になります。

f:id:sugimomoto:20180114125814p:plain

もう一つは、前述したヘッダと明細のような関係性で、以下のようなJsonを扱う場合です。

ショップの情報に、評価情報として、[grades]という[date][grade][score]の3種類のオブジェクトを配列構造で保持しているデータです。

{
  "_id" : ObjectId("568c37b748ddf53c5ed98932"),
  "address" : {
    "building" : "1007",
    "coord" : [-73.856077, 40.848447],
    "street" : "Morris Park Ave",
    "zipcode" : "10462"
  },
  "borough" : "Bronx",
  "cuisine" : "Bakery",
  "grades" : [{
      "date" : ISODate("2014-03-03T00:00:00Z"),
      "grade" : "A",
      "score" : 2
    }, {
      "date" : ISODate("2013-09-11T00:00:00Z"),
      "grade" : "A",
      "score" : 6
    }, {
      "date" : ISODate("2013-01-24T00:00:00Z"),
      "grade" : "A",
      "score" : 10
    }, {
      "date" : ISODate("2011-11-23T00:00:00Z"),
      "grade" : "A",
      "score" : 9
    }, {
      "date" : ISODate("2011-03-10T00:00:00Z"),
      "grade" : "B",
      "score" : 14
    }],
  "name" : "Morris Park Bake Shop",
  "restaurant_id" : "30075445"
}

これをCData Driverでは垂直フラット化と称して、以下のような疑似テーブルクエリを描くことで、同一のドキュメントデータながらも、明細データにアクセスするようにクエリすることが可能です。

SELECT * FROM [restaurants.grades]

f:id:sugimomoto:20180114125822p:plain

これを最終的にはJoinして、以下のような表現をすることが可能になります。

SELECT [restaurants].[restaurant_id], [restaurants.grades].* FROM [restaurants.grades] JOIN [restaurants] WHERE [restaurants].name = 'Morris Park Bake Shop'

f:id:sugimomoto:20180114125826p:plain

まさにヘッダーと明細ライクな使い方ですね。

もうちょっとアドホックにクエリアクセスしてみる

ちなみに、もうちょっとアドホックJsonオブジェクトへクエリすることも可能です。

以下のようなドキュメントに対して、

{
  "address": {
    "building": "1007",
    "coord": [
      -73.856077,
      40.848447
    ],
    "street": "Morris Park Ave",
    "zipcode": "10462"
  },
  "borough": "Bronx",
  "cuisine": "Bakery",
  "grades": [
    {
      "grade": "A",
      "score": 2,
      "date": {
        "$date": "1393804800000"
      }
    },
    {
      "date": {
        "$date": "1378857600000"
      },
      "grade": "B",
      "score": 6
    },
    {
      "score": 10,
      "date": {
        "$date": "1358985600000"
      },
      "grade": "C"
    }
  ],
  "name": "Morris Park Bake Shop",
  "restaurant_id": "30075445"
}

例えば[address]オブジェクト配下の[building]もしくは[grades]配列の1番目の[grade]情報が欲しいとなれば、そのままクエリでJsonオブジェクトのアクセスのような形で以下のようにクエリすることが可能です。

SELECT [address.building], [grades.1.grade] FROM restaurants WHERE restaurant_id = '30075445'

f:id:sugimomoto:20180114125844p:plain

おわりに

今回はデータ構造に重点を追いて書いてきたのですが、これ以外にも触れるべき点が結構あります。

例えばスキーマ検出方法が、最初のドキュメント100件をベースに算出していることや、グルーピング、別コレクションのJOIN方法などなど。

英語ですが、以下のPDFでCData Driverが NoSQLデータベースに対して、どのように変換アプローチを試みているのかに関する情報が記載されていますので、是非参考までにどうぞ。

https://www.cdata.com/resources/SQL%20for%20NoSQL.pdf