JanusGraphのスキーマ④ - Schema Constraints

スキーマの何がありがたいかと言えば、意図しないデータの混入を防ぐことができるという点である。スキーマを定義する作業はめんどくさいものだけれども、あとあと自分を守ってくれる。

JanusGraphにはSchema Constraints(スキーマ制約)という機能があり、予め設定したラベル/プロパティしか登録できないという制約を設けることができる。

## 設定

Schema Constraintsに関する設定は以下の2つ。

|変数|説明|デフォルト値|
|---|---|---|
|schema.constraints|グラフによって使用されるスキーマ制約を設定する。値が`true`でかつ`schema.default`が`none`に設定されている場合、スキーマ違反が発生した場合に`IllegalArgumentException`がthrowされる。<br>値が`true`でかつ`schema.default`が`none`でない場合は、`schema.default`の設定に従ってスキーマ制約が自動的に生成される。<br>値が`false`(デフォルト値)の場合はスキーマ制約は適用されない。|false|
|schema.default|グラフによって作用される`DefaultSchemaMaker`の挙動を設定する。<br> - `default`: データ挿入時に自動的にスキーマを生成する<br> - `none`: 自動的なスキーマ生成は行わない<br> - `logging`: `default`とほぼ同様だが、スキーマ生成時にメッセージを出す<br>その他、`DefaultSchemeMaker`のインターフェースを実装したクラス名(+パッケージ名)を指定することもできる|default|

この2つの変数の組み合わせによって挙動が変わる。制約を有効にするには

- schema.constraints=true
- schema.default=none

とすればよい。

## サーバー設定方法

ダウンロードしたアーカイブを使うか、Dockerを使うかで設定方法が異なる。

### アーカイブを使う場合

自分で設定ファイルを作成する。`conf/gremlin-server`ディレクトリに`myserver.yaml`と`myserver.properties`の2つのファイルを用意する。

```yaml:conf/gremlin-server/myserver.yaml
host: 0.0.0.0
port: 8182
scriptEvaluationTimeout: 30000
channelizer: org.apache.tinkerpop.gremlin.server.channel.WebSocketChannelizer
graphs: {
    graph: conf/gremlin-server/myserver.properties}
scriptEngines: {
    gremlin-groovy: {
        plugins: { org.janusgraph.graphdb.tinkerpop.plugin.JanusGraphGremlinPlugin: {},
                   org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {},
                   org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {},
                   org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]},
                   org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}}
serializers:
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { serializeResultToString: true }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
    # Older serialization versions for backwards compatibility:
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoLiteMessageSerializerV1d0, config: {ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { serializeResultToString: true }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerGremlinV2d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerGremlinV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistryV1d0] }}
    - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistryV1d0] }}
processors:
    - { className: org.apache.tinkerpop.gremlin.server.op.session.SessionOpProcessor, config: { sessionTimeout: 28800000 }}
    - { className: org.apache.tinkerpop.gremlin.server.op.traversal.TraversalOpProcessor, config: { cacheExpirationTime: 600000, cacheMaxSize: 1000 }}
metrics: {
    consoleReporter: {enabled: true, interval: 180000},
    csvReporter: {enabled: true, interval: 180000, fileName: /tmp/gremlin-server-metrics.csv},
    jmxReporter: {enabled: true},
    slf4jReporter: {enabled: true, interval: 180000},
    gangliaReporter: {enabled: false, interval: 180000, addressingMode: MULTICAST},
    graphiteReporter: {enabled: false, interval: 180000}}
maxInitialLineLength: 4096
maxHeaderSize: 8192
maxChunkSize: 8192
maxContentLength: 65536
maxAccumulationBufferComponents: 1024
resultIterationBatchSize: 64
writeBufferLowWaterMark: 32768
writeBufferHighWaterMark: 65536
```
長いが、もともとあったファイルをコピペして、graph(6行目)のところだけ自前のファイルパスに書き換えただけである。

```ini:conf/gremlin-server/myserver.properties
gremlin.graph=org.janusgraph.core.JanusGraphFactory
storage.backend=berkeleyje
storage.directory=db/berkeley

schema.constraints=true
schema.default=none
```

そしてサーバーを起動(Windowsの人はgremlin-server.batを使用のこと)

```sh
$ bin/gremlin-server.sh conf/gremlin-server/myserver.yaml
```

### Dockerの場合

コンテナに環境変数を追加する。Docker-Composeの設定ファイルを以下に示す。

```yml:docker-compose.yml
version: '3'
services:
    schemajanus:
        image: janusgraph/janusgraph
        ports:
            - 8182:8182
        environment:
            - janusgraph.schema.default=none
            - janusgraph.schema.constraints=true
        volumes:
            - janusgraph-schema-test:/var/lib/janusgraph
volumes:
    janusgraph-schema-test:

```

立ち上げる場合は

```sh
$ docker-compose up -d
```

停止する場合は

```sh
$ docker-compose down
```

データを抹消したい(リセットしたい)場合は

```sh
$ docker-compose down -v
```

## 動作確認

### Vertex Label - Property Constraints

スキーマを作ってみて、どのような挙動をするのか確認してみる。まず、スキーマを定義する。いつものようにGremlinコンソールからリモート接続してコマンドを打ち込む(公式ドキュメントそのまま)。

```groovy
gremlin> :remote connect tinkerpop.server conf/remote.yaml session
==>Configured localhost/127.0.0.1:8182-[4f6e7645-3836-4108-bcc2-e44c685e6147]
gremlin> :> m = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@47f525aa
gremlin> :> person = m.makeVertexLabel('person').make()
==>person
gremlin> :> name = m.makePropertyKey('name').dataType(String.class).cardinality(org.janusgraph.core.Cardinality.SET).make()
==>name
gremlin> :> birthDate = m.makePropertyKey('birthDate').dataType(Long.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()
==>birthDate
gremlin> :> m.addProperties(person, name, birthDate)
==>person
gremlin> :> m.commit()
==>null
```

制約を作っているのは、`addProperties`。ここに指定したラベルとプロパティが関連付けられる模様。

まず、頂点とプロパティを挿入してみる。

```groovy
gremlin> :> g.addV("person").property("name", "bob").property("birthDate", 31)
==>v[4296]
```

登録済みのラベルとプロパティは当然OK。

```groovy
gremlin> :> g.addV("animal")
Vertex Label with given name does not exist: animal
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

登録していないラベルはダメ。よし。

```groovy
gremlin> :> g.addV("person").property("country", "japan")
Property Key with given name does not exist: country
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

登録していないプロパティもダメ。よしよし。

```groovy
gremlin> :> g.addV("person")
==>v[4128]
```

むむむ。プロパティなしでも登録できてしまう。調べてみたが今のところ、定義していないプロパティを使うのはNGだが、定義したプロパティを使わないのはOKらしい。プロパティ毎に`なくてもよい`か`必須`かを指定できるといいのだけれども……。

もう1つラベルとプロパティを追加して、以下のような組み合わせにしてみる。

- ラベルperson - プロパティname - プロパティbirthDate
- ラべルlang - プロパティname - プロパティtype

```groovy
gremlin> :> lang = m.makeVertexLabel("lang").make()
==>lang
gremlin> :> type = m.makePropertyKey("type").dataType(String.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()
==>type
gremlin> :> name = m.getPropertyKey("name")
==>type
gremlin> :> m.addProperties(lang, name, type)
==>lang
gremlin> :> m.commit()
```

```groovy
gremlin> :> g.addV("lang").property("name", "java").property("type", "procedual")
==>v[8208]
gremlin> :> g.addV("lang").property("birthDate", 30)
Property Key constraint does not exist for given Vertex Label [lang] and property key [birthDate].
Type ':help' or ':h' for help.
Display stack trace? [yN]
gremlin> :> g.addV("person").property("type", "lazy")
Property Key constraint does not exist for given Vertex Label [person] and property key [type].
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

それぞれのラベルについて、`addProperties`で関連付けていないプロパティを付けようとするとエラーによる。グッド！

### Edge Label - Property Constraints

エッジのラベルについても確認してみる。

```groovy
gremlin> :> m = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@3baddb94
gremlin> :> follow = m.makeEdgeLabel("follow").multiplicity(org.janusgraph.core.Multiplicity.MULTI).make()
==>follow
gremlin> :> name = m.getPropertyKey("name")
==>name
gremlin> :> m.addProperties(follow, name)
An Edge [follow] can not have a property [name] with the cardinality [SET].
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

公式ドキュメントの通りにやると怒られるんですが……。エッジに対してカーディナリティが`SET`のプロパティは付けられない、と。`SINGLE`のプロパティを新しく作ってやり直します。

```groovy
gremlin> :> date_from = m.makePropertyKey("date_from").dataType(String.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()
==>date_from
gremlin> :> m.addProperties(follow, date_from)
==>follow
gremlin> :> m.commit()
==>null
```

それで、適当に頂点を2つ用意して、エッジを追加、と。

```groovy
gremlin> :> alice = g.addV("person").property("name", "alice").next()
==>v[4240]
gremlin> :> ellie = g.addV("person").property("name", "ellie").next()
==>v[8336]
gremlin> :> g.addE("follow").from(alice).to(ellie)
Connection constraint does not exist for given Edge Label [follow], outgoing Vertex Label [person] and incoming Vertex Label [person]
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

今度はConnection Constraintを先に定義しろと怒られた。さもありなん。先にそちらの対応を行う。

### Connection Constraints

頂点とエッジのつながりのルールを定義する。

ここまでの例で作成した頂点ラベル`person`とエッジラベル`follow`を接続する。

- 1.(person) -> 2.[follow] -> 3.(person)

```groovy
gremlin> :> m = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@ab1b7ec
gremlin> :> person = m.getVertexLabel("person")
==>person
gremlin> :> follow = m.getEdgeLabel("follow")
==>follow
// エッジとその両端の頂点の関係を定義する。引数は2.エッジ, 1.元の頂点, 3.先の頂点の順番
gremlin> :> m.addConnection(follow, person, person)
==>follow
gremlin> :> m.commit()
==>null
```

と、ここで問題があり、このままエッジを追加しても`Connection constraint does not exist`とエラーが発生した。サーバーを再起動することで解消した([参考](https://github.com/JanusGraph/janusgraph/issues/1319))。

```groovy
gremlin> :> alice = g.V().has("name", "alice").next()
==>v[4240]
gremlin> :> ellie = g.V().has("name", "ellie").next()
==>v[8336]
gremlin> :> g.addE("follow").from(alice).to(ellie)
==>e[odxcc-38g-27th-oe09c][4192-follow->8336]
```

ようやくエッジが追加できるようになった。1つ手前に戻って、プロパティの追加を確認してみる。

```groovy
//関連付け済みのプロパティdate_fromを試す
gremlin> :> g.addE("follow").from(alice).to(ellie).property("date_from", "2020/04/01")
==>e[odxqk-38g-27th-oe09c][4192-follow->40964304]  // OK
//関連付けしていないプロパティbirthDateを試す
gremlin> :> g.addE("follow").from(alice).to(ellie).property("birthDate", 30)
Property Key constraint does not exist for given Edge Label [follow] and property key [birthDate].
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

ちゃんと機能している。

## 自動化

ここまで大分ごちゃごちゃといろいろ作業してしまったため、整理を兼ねて、自動化のスクリプトを組んだ(使い方は[こちらを参照](https://qiita.com/chromia/items/8c70ca78df62c47dcae2#%E8%87%AA%E5%8B%95%E5%8C%96))。

```groovy:register_schema.groovy
:remote connect tinkerpop.server conf/remote.yaml session
:> m = graph.openManagement()

// Vertex Labels
:> person = m.makeVertexLabel("person").make()
:> lang = m.makeVertexLabel("lang").make()

// Edge Labels
:> follow = m.makeEdgeLabel("follow").multiplicity(org.janusgraph.core.Multiplicity.SIMPLE).make()
:> use = m.makeEdgeLabel("use").multiplicity(org.janusgraph.core.Multiplicity.SIMPLE).make()

// Properties
:> name = m.makePropertyKey("name").dataType(String.class).cardinality(org.janusgraph.core.Cardinality.SET).make()
:> date_from = m.makePropertyKey("date_from").dataType(String.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()
:> birth_month = m.makePropertyKey("birth_month").dataType(Byte.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()
:> birth_day = m.makePropertyKey("birth_day").dataType(Byte.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()
:> lang_type = m.makePropertyKey("lang_type").dataType(String.class).cardinality(org.janusgraph.core.Cardinality.SINGLE).make()

// Vertex Label - Property Constraints
:> m.addProperties(person, name, birth_month, birth_day)
:> m.addProperties(lang, name, lang_type)

// Edge Label - Property Constraints
:> m.addProperties(follow, date_from)

// Connection Constraints
:> m.addConnection(follow, person, person)
:> m.addConnection(use, person, lang)

:> m.commit()
:remote close

```

カスタマイズして使って欲しい。データを追加するスクリプトも用意した。

```groovy:register_data.groovy
:remote connect tinkerpop.server conf/remote.yaml session

// Add Vertices
:> bob = g.addV("person").property("name", "bob").property("birth_month", 2).property("birth_day", 11).next()
:> james = g.addV("person").property("name", "james").property("birth_month", 5).property("birth_day", 24).next()
:> alice = g.addV("person").property("name", "alice").property("birth_month", 8).property("birth_day", 31).next()
:> ellie = g.addV("person").property("name", "ellie").property("birth_month",12).property("birth_day", 25).next()
:> java = g.addV("lang").property("name", "Java").property("lang_type", "procedual").next()
:> haskell = g.addV("lang").property("name", "Haskell").property("lang_type", "functional").next()

// Add Edges
:> g.addE("follow").from(bob).to(alice).property("date_from", "2015/11/07")
:> g.addE("follow").from(alice).to(ellie).property("date_from", "2019/12/12")
:> g.addE("use").from(james).to(java)
:> g.addE("use").from(james).to(haskell)

// Commit Transation
:> graph.tx().commit()

:remote close
```

## あとがき

今回でスキーマ関連はおしまい。次はトランザクションについての理解を深める。