mike-neckのブログ

Java or Groovy or Swift or Golang

datomicチュートリアル12日目 #datomic

今日はスキーマ定義を行います。

スキーマ定義の方法

RDBMSなどではスキーマを定義するために、専用のSQL文があります(CREATE TABLE ...)。しかし、datomicではそのような専用のトランザクションdata structureなどはありません。あくまでデータ追加のトランザクションdata structureを使うことでスキーマ定義を行います。

したがって、構文(マップの場合)は次のようになります。

[
    [
        :db/id temp-id,
        :db/ident attribute-keyword,
        :db/valueType type,
        :db/cardnality cardinality,
        :db.install/_attribute :db.part/db
    ]
]

では下記に示す簡単なスキーマを定義してみましょう。

エンティティ アトリビュート カーディナリティ
student name string One
static final String URI = 'datomic:mem://tutorial'
Connection connection
@Before
void prepare() {
    Peer.createDatabase(URI)
    connection = Peer.connect(URI)
}

static final String DB_PARTITION = ':db.part/db'
@Test
void 簡単なスキーマを作ってスキーマの定義を確認する() {
    // トランザクションデータ
    def tx = [[
            // IDは一時IDをDBパーティション(:db.part/db)から取得
            ':db/id': Peer.tempid(DB_PARTITION),
            // 属性名
            ':db/ident': ':student/name',
            // 属性の型
            ':db/valueType': ':db.type/string',
            // カーディナリティ
            ':db/cardinality': ':db.cardinality/one',
            // 属性情報が所属するパーティション
            ':db.install/_attribute': ':db.part/db'
    ]]
    // トランザクション実行
    connection.transact(tx).get()
    def db = connection.db()
    // スキーマ自体を参照するクエリー
    def query = $/[
:find
    ?attr
    ?type
    ?cad
:where
    [?d :db/ident :student/name]
    [?d :db/ident ?attr]
    [?d :db/valueType ?type]
    [?d :db/cardinality ?cad]
    ]/$ as String
    // クエリー発行
    def set = Peer.query(query, db)[0]
    // 表示
    set.each {
        def v = it.getClass() == Long ? db.ident(it): it
        log "result: [${v}]"
    }
}

これを実行すると次のようなログが出力されます。

result: [:student/name]
result: [:db.type/string]
result: [:db.cardinality/one]

ちゃんとスキーマとして定義されたようです。

では、上記のスキーマ定義に対して実際にデータをいれこんでみます。

@Test
void 簡単なスキーマを作ってデータを入れてみる() {
    // トランザクションデータ
    def tx = [[
            // IDは一時IDをDBパーティション(:db.part/db)から取得
            ':db/id': Peer.tempid(DB_PARTITION),
            // 属性名
            ':db/ident': ':student/name',
            // 属性の型
            ':db/valueType': ':db.type/string',
            // カーディナリティ
            ':db/cardinality': ':db.cardinality/one',
            // 属性情報が所属するパーティション
            ':db.install/_attribute': ':db.part/db'
    ]]
    // トランザクション実行
    connection.transact(tx).get()
    // サンプルデータ作成
    def add = [[
            ':db/add', Peer.tempid(USER_PARTITION),
            ':student/name', 'Hoge'
    ]]
    // トランザクション実行
    def db = connection.transact(add).get().get(Connection.DB_AFTER)
    // データを取得
    def results = Peer.query('[:find [?name ...] :where [_ :student/name ?name]]', db)
    // テスト
    assert results.size() == 1
    assert results[0] == 'Hoge'
}

このテストは普通にパスします。データ追加、参照もうまくいったようです。

スキーマ定義

ドキュメントによればスキーマを定義する場合は次の4つの項目を定義する必要があります。

  • :db/ident - エンティティ名/属性名を定義します
  • :db/valueType - データの型を定義します
  • :db/cardinality - カーディナリティを定義します

1エンティティ名/属性名

これはキーワードの形式で定義します。

これまでも何度か見ていますが、キーワードは次のような文字列です

  • :で始まる
  • 数値以外の英字および. * + ! - _ ? $ % & = < >が続く
  • .+-で始まった場合は次の文字は英字
  • /は一回だけ使える(エンティティ名と属性名の境界として用いる)

なお、詳しくはednを読んでください

データの型

型は次のものから選べます。

カーディナリティ

カーディナリティのOneとManyはそれぞれ次の値で設定できます。

  • :db.cardinality/one
  • :db.cardinality/many

いろいろなデータ型を試してみる

では、次のようなスキーマを定義して、データを流し込んでみたいと思います。

アトリビュート カーディナリティ
:patient/name string One
:patient/married boolean One
:patient/age long One
:patient/eyesight double One
:patient/first-visit instant One
:patient/home-page uri One
@Test
void 様々なデータタイプを試してみる() {
    // スキーマとサンプルデータ
    def schema = [
            [attr: 'name', type: 'string', values: ['山田太郎', '佐藤花子']],
            [attr: 'married', type: 'boolean', values: [false, true]],
            [attr: 'age', type: 'long', values: [30, 19]],
            [attr: 'eyesight', type: 'double', values: [0.1d, 2.0d]],
            [attr: 'first-visit', type: 'instant', values: [
                    LocalDateTime.parse('2014/12/31 09:00:00', PATTERN).toDate(),
                    LocalDateTime.parse('2014/08/02 09:00:00', PATTERN).toDate()]],
            [attr: 'home-page', type: 'uri', values: [
                    new URI('http://localhost:8000/test/yamada'),
                    new URI('http://localhost:8000/test/sato')]]
    ]
    // トランザクションdata structureに変換
    def schemaTx = schema.collect {
        [
                ':db/id': Peer.tempid(DB_PARTITION),
                ':db/ident': ":patient/${it.attr}" as String,
                ':db/valueType': ":db.type/${it.type}" as String,
                ':db/cardinality': ':db.cardinality/one',
                ':db.install/_attribute': ':db.part/db'
        ]
    }
    // トランザクション実行
    connection.transact(schemaTx).get()
    log 'schema defined'
    // サンプルデータ
    def tx = (0..1).collect { idx ->
        def map = schema.collect { s ->
            [attr: ":patient/${s.attr}" as String, value: s.values[idx]]
        }.collectEntries {
            [it.attr, it.value]
        }
        map += [':db/id': Peer.tempid(USER_PARTITION)]
        map
    }
    // データ追加
    def db = connection.transact(tx).get().get(Connection.DB_AFTER)
    // データ検証
    def items = schema.collect { "?${it.attr}" }.join(' ')
    def condition = schema.collect { "[?p :patient/${it.attr} ?${it.attr}]" }.join(' ')
    def results = Peer.query("[:find $items :where $condition]" as String, db)
    assert results.size() == 2
    results.each {vec ->
        def record = new IntRange(false, 0, vec.size()).collect {
            "[${vec[it]}]"
        }.join(', ')
        log "${record}"
    }
}

これを実行すると、次のようなログが表示されます。

[佐藤花子], [true], [19], [2.0], [Sat Aug 02 09:00:00 JST 2014], [http://localhost:8000/test/sato]
[山田太郎], [false], [30], [0.1], [Wed Dec 31 09:00:00 JST 2014], [http://localhost:8000/test/yamada]

普通のデータベースのように保存するようなデータがちゃんと保存できていることがわかります。

なお、上記のコードでLocalDateTime#toDate()というメソッドを呼んでいますが、そのようなメソッドはありません。次のコードで拡張しています。

@BeforeClass
static void expand() {
    LocalDateTime.metaClass.define {
        toDate = {
            Date.from(delegate.toInstant(ZoneOffset.ofHours(9)))
        }
    }
}

以上。次回はスキーマ定義のオプショナルな属性についてやります。