今日は参照型の定義と注意点を書きます。
参照型(: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 に参照 |
: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
の値が返ってきます。ここから、定義したenumのKeyword
を取得したい場合は、次のように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アプリに適用してみる
これらについてはやってみたら、都度ブログを書こうと思います。
おわり