今日はスキーマ定義を行います。
スキーマ定義の方法
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))) } } }
以上。次回はスキーマ定義のオプショナルな属性についてやります。