今日はスキーマ定義を行います。
スキーマ定義の方法
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を読んでください
データの型
型は次のものから選べます。
:db.type/keyword- キーワード/clojure.lang.Keywordにマッピングされます:db.type/string- 文字列/java.lang.Stringにマッピングされます:db.type/boolean- 真偽値/booleanにマッピングされます:db.type/long- 整数/longにマッピングされます:db.type/bigint- 整数/java.math.BigIntegerにマッピングされます:db.type/float- 単精度浮動小数点数/floatにマッピングされます:db.type/double- 倍精度浮動小数点数/doubleにマッピングされます:db.type/bigdec- 任意制度の浮動小数点数/java.math.BigDecimalにマッピングされます:db.type/ref- 参照型/別のエンティティへの参照:db.type/instant- 時刻/1970/1/1 0:0:0 UTCからのミリ秒で、java.util.Dateにマッピングされます:db.type/uuid- UUID/java.util.UUIDにマッピングされます:db.type/uri- URI/java.net.URIにマッピングされます:db.type/bytes- バイト列/byte[]にマッピングされます。画像などのバイナリーを持つための型
カーディナリティ
カーディナリティの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))) } } }
以上。次回はスキーマ定義のオプショナルな属性についてやります。