2
1

More than 3 years have passed since last update.

JanusGraphのスキーマ④ - Schema Constraints

Posted at

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

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

設定

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

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

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

  • schema.constraints=true
  • schema.default=none

とすればよい。

サーバー設定方法

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

アーカイブを使う場合

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

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行目)のところだけ自前のファイルパスに書き換えただけである。

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を使用のこと)

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

Dockerの場合

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

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:

立ち上げる場合は

$ docker-compose up -d

停止する場合は

$ docker-compose down

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

$ docker-compose down -v

動作確認

Vertex Label - Property Constraints

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

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。ここに指定したラベルとプロパティが関連付けられる模様。

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

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

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

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

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

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]

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

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

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

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

  • ラベルperson - プロパティname - プロパティbirthDate
  • ラべルlang - プロパティname - プロパティtype
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()
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

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

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のプロパティを新しく作ってやり直します。

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つ用意して、エッジを追加、と。

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)
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とエラーが発生した。サーバーを再起動することで解消した(参考)。

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つ手前に戻って、プロパティの追加を確認してみる。

//関連付け済みのプロパティ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]

ちゃんと機能している。

自動化

ここまで大分ごちゃごちゃといろいろ作業してしまったため、整理を兼ねて、自動化のスクリプトを組んだ(使い方はこちらを参照)。

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

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

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

あとがき

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1