mike-neckのブログ

Java or Groovy or Swift or Golang

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

今日は参照型の定義と注意点を書きます。

参照型(:db.type/ref)には次の二種類の参照型があります。

  • enumへの参照型
  • 別のエンティティへの参照型

enumはシステムが定義した:db/identという属性に所属する定数値で、その型はclojure.lang.Keyword型にマッピングされます。直前で参照型は二種類あると書いていますが、enumへの参照とは:dbエンティティの:db/identアトリビュートへの参照になりますので、実質一種類であるとも言えます。


enumの定義・参照

enum値の定義自体は簡単です。

ユーザーパーティション:db/ident属性の値としてキーワードを追加するだけです。

[
  {
    :db/id #db/id[:db.part/user -100],
    :db/ident :your.enum/value
  }
]

または

[
  [
    :db/add
    #db/id[:db.part/user -100]
    :db/ident
    :your.enum/value
  ]
]

では、次のようなスキーマ定義 + enum定義のサンプルを実行してみます。

スキーマ

エンティティ アトリビュート カーディナリティ
person name :db.type/string One -
person birth :db.type/long One -
person sex :db.type/ref One :person.sexに参照

enum

  • :person.sex/woman
  • :person.sex/man
@Test
void enumのを定義する() {
    //enumの定義
    def enumDef = [
            [':db/id': Peer.tempid(USER_PARTITION), ':db/ident': ':person.sex/woman'],
            [':db/id': Peer.tempid(USER_PARTITION), ':db/ident': ':person.sex/man']
    ]
    connection.transact(enumDef).get()
    //スキーマ定義
    def definition = [
            SchemaDefinition.entity('person').attr('name').stringType().one().asMap(),
            SchemaDefinition.entity('person').attr('birth').longType().one().asMap(),
            SchemaDefinition.entity('person').attr('sex').enumType().one().asMap()
    ]
    connection.transact(definition).get()
    //データ作成
    def tx = [
            ListTransaction.useId(-1).create(entity: 'person', attr: 'name', value: '山田花子'),
            ListTransaction.useId(-1).create(entity: 'person', attr: 'birth', value: 1995),
            ListTransaction.useId(-1).create(entity: 'person', attr: 'sex', value: ':person.sex/woman'),
            ListTransaction.useId(-2).create(entity: 'person', attr: 'name', value: '山本太郎'),
            ListTransaction.useId(-2).create(entity: 'person', attr: 'birth', value: 1998),
            ListTransaction.useId(-2).create(entity: 'person', attr: 'sex', value: 'person.sex/man')
    ]
    connection.transact(tx.collect{it.transaction}).get()
    //クエリー実行
    def db = connection.db()
    def results = Peer.query('''[
:find
    ?name
    ?birth
    ?key
:where
    [?p :person/name ?name]
    [?p :person/birth ?birth]
    [?p :person/sex ?sex]
    [?sex :db/ident ?key]]''', db)
    assert results.size() == 2
    results.each {vec ->
        if (vec[0] == '山田花子') assert db.ident(vec[1]).toString() == ':person.sex/woman'
        if (vec[0] == '山本太郎') assert db.ident(vec[1]).toString() == ':person.sex/man'
        log "name: [${vec[0]}], birth: [${vec[1]}], sex: [${vec[2]}]"
    }
}

上記のサンプルコードではテストデータとして次のようなデータを投入しています。

name birth sex
山田花子 1995 :person.sex/woman
山本太郎 1998 :person.sex/man

クエリーは名前、生年、性別を取得するだけのものです。

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

name: [山田花子], birth: [1995], sex: [:person.sex/woman]
name: [山本太郎], birth: [1998], sex: [:person.sex/man]

クエリーの途中で次のように変数?sexを拘束しています。

[:p :person/sex ?sex]

これをそのまま取得した場合はlongの値が返ってきます。ここから、定義したenumKeywordを取得したい場合は、次のようにDatabase#ident(long)メソッドを用います。

db.ident(vec[2])

クエリーの中では次のように別の変数?key?sexに拘束された値から:db/ident属性の値を取得しているので、上記クエリーではKeyword型になって返ってきます。

[?sex :db/ident ?key]

別エンティティへ参照するアトリビュートの定義

別エンティティへ参照するアトリビュートの定義は先ほどのenumへの参照と同じで、属性の型に:db.type/refを指定します。

では次のようなスキーマ定義をするサンプルを実行してみます。

entity attribute type cardinality
customer name :db.type/string one
customer sex :db.type/ref one
customer address :db.type/ref many
address city :db.type/string one

一人のお客さんに複数の住所(お届け先)が紐付いているようなモデルです。

@Test
void 別エンティティへ参照する() {
    def schema = [
            SchemaDefinition.entity('customer').attr('name')
                    .stringType().one().asMap(),
            SchemaDefinition.entity('customer').attr('sex')
                    .enumType().one().asMap(),
            SchemaDefinition.entity('customer').attr('address')
                    .refType().many().asMap(),
            SchemaDefinition.entity('address').attr('city')
                    .stringType().one().asMap(),
            [':db/id': Peer.tempid(USER_PARTITION), ':db/ident': ':customer.sex/woman'],
            [':db/id': Peer.tempid(USER_PARTITION), ':db/ident': ':customer.sex/man']
    ]
    connection.transact(schema).get()
    def tx = [
            [':db/id': Peer.tempid(USER_PARTITION, -100), ':address/city': '東京'],
            [':db/id': Peer.tempid(USER_PARTITION, -200), ':address/city': '大阪'],
            [':db/id': Peer.tempid(USER_PARTITION, -300), ':address/city': '名古屋'],
            [':db/id': Peer.tempid(USER_PARTITION), ':customer/name': '山田太郎',
             'customer/sex': ':customer.sex/man', ':customer/address': [
                    Peer.tempid(USER_PARTITION, -100),
                    Peer.tempid(USER_PARTITION, -200),
                    Peer.tempid(USER_PARTITION, -300)]],
            [':db/id': Peer.tempid(USER_PARTITION, -400), ':address/city': '東京'],
            [':db/id': Peer.tempid(USER_PARTITION), ':customer/name': '山本華子',
             'customer/sex': ':customer.sex/woman', ':customer/address':
                     Peer.tempid(USER_PARTITION, -400)]
    ]
    connection.transact(tx).get()
    def db = connection.db()
    def results = Peer.query('''[:find
    ?name
    ?sex
    ?city
:where
    [?c :customer/name ?name]
    [?c :customer/sex ?s]
    [?s :db/ident ?sex]
    [?c :customer/address ?a]
    [?a :address/city ?city]]''', db)
    assert results.size() == 4
    results.each {vec ->
        log "name:[${vec[0]}], sex: [${vec[1]}], city: [${vec[2]}]"
    }
}

上記のサンプルコードでは「山田太郎」は「東京」「名古屋」「大阪」にお届け先を持つお客様であり、「山本華子」は「東京」にお届け先を持つお客様です。

このコードの実行後のログは次のようになります(assertはパスします)。

name:[山田太郎], sex: [:customer.sex/man], city: [名古屋]
name:[山本華子], sex: [:customer.sex/woman], city: [東京]
name:[山田太郎], sex: [:customer.sex/man], city: [東京]
name:[山田太郎], sex: [:customer.sex/man], city: [大阪]

予定通り、別エンティティへの参照ができていることがわかります。

参照型は型安全ではないことに注意

最後に、参照型はまったくもって型安全ではないことを示します。

ここまでの二つのサンプルでは行儀よく、enumへの参照はenumに参照するように、別エンティティへの参照は最初から決められた通りのエンティティへ参照するようにコードを記述していました。

ただ、最初の方に述べた通り、enumへの参照型も、別エンティティへの参照も参照型であることに変わりはありません。したがって、別エンティティを参照するはずの属性にenumへの参照を入れてしまうこともコードの間違いによって発生します。

例えば、先ほどと同じスキーマ定義で、「山本華子」さんの本来は:addressエンティティへ参照するべき:customer/addressに誤って:customer.sex/manへの参照を入れてしまったらどうなるか試してみましょう。

    def tx = [
            [':db/id': Peer.tempid(USER_PARTITION, -100), ':address/city': '東京'],
            [':db/id': Peer.tempid(USER_PARTITION, -200), ':address/city': '大阪'],
            [':db/id': Peer.tempid(USER_PARTITION, -300), ':address/city': '名古屋'],
            [':db/id': Peer.tempid(USER_PARTITION), ':customer/name': '山田太郎',
             'customer/sex': ':customer.sex/man', ':customer/address': [
                    Peer.tempid(USER_PARTITION, -100),
                    Peer.tempid(USER_PARTITION, -200),
                    Peer.tempid(USER_PARTITION, -300)]],
            [':db/id': Peer.tempid(USER_PARTITION, -400), ':address/city': '東京'],
            [':db/id': Peer.tempid(USER_PARTITION), ':customer/name': '山本華子',
             'customer/sex': ':customer.sex/woman', ':customer/address': ':customer.sex/man']
    ]
    connection.transact(tx).get()

先ほどのデータ登録トランザクションの最後の部分を

':customer/address': Peer.tempid(USER_PARTITION, -400)

から

':customer/address': ':customer.sex/man'

に変更しています。

次のクエリーを流してみます。

[
:find
    ?name
    ?address
:where
    [?c :customer/name ?name]
    [?c :customer/address ?address]]

結果として取得したオブジェクトresultsに対して、次の通りログ出力をさせます。

results.each {vec ->
    log "name: [${vec[0]}], value by db.ident: [${db.ident(vec[1])}], key set by db.entity: [${db.entity(vec[1]).keySet()}]"
}

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

name: [山本華子], value by db.ident: [:customer.sex/man], key set by db.entity: [[:db/ident]]
name: [山田太郎], value by db.ident: [null], key set by db.entity: [[:address/city]]
name: [山田太郎], value by db.ident: [null], key set by db.entity: [[:address/city]]
name: [山田太郎], value by db.ident: [null], key set by db.entity: [[:address/city]]

間違えて変な値を設定してしまった「山本華子」さんのデータが明らかに「山田太郎」さんのものと異なることがわかります。

これは通常のRDBでも別のテーブルへの外部参照するカラムにdecimalなどの値が使われるように、:db.type/refにはlongの値が設定されます。強い外部参照制約を持たないdatomicにおいては、このような参照誤りをエラーとしてはねつけることができないため、おかしなデータになっているのが後からわかるということになってしまいます。この辺りの不便さはアプリケーション側で制御するか、datomicに強い外部参照制約が実装されるのを待つしかないようです。


以上で、おおよそDatomic Tutorialに書かれている内容の学習が完了です。

さて、今後ですが、Datomicに対して僕が試してみたいことが幾つかあります。

  • スキーマ変更のやり方(RDBより優れている部分、劣っている部分がありそう)
  • データストアとの連携(RDBMS、mongo、cassandra、dynamoDb(余裕があれば))方法
  • データストアに格納されたデータの実際の格納状況の確認
  • 他の言語からの接続(Java/Scala/Web API)
  • キャパシティプランニングについて
  • 実際にJava EEアプリに適用してみる

これらについてはやってみたら、都度ブログを書こうと思います。


おわり