JanusGraphのスキーマ② - Property Key Data Type



## データタイプ

JanusGraphのスキーマでは、以下のデータタイプに対応している。

- String: 文字列型
- Character: 文字型
- Boolean: ブール値
- Byte: 符号付き整数(-128～127)
- Short: 符号付き整数(-32768～32767)
- Integer: 符号付き整数(-2147483648～2147483647)
- Long: 符号付き整数(-9223372036854775808～9223372036854775807)
- Float: 4バイト精度浮動小数点数
- Double: 8バイト精度浮動小数点数
- Date: Date型(java.lang.Date)
- [Geoshape](https://docs.janusgraph.org/index-backend/search-predicates/#geoshape-data-type): 位置座標
- UUID: 識別子型(java.util.UUID)


それぞれの挙動を試すために、以下のようにデータタイプに対応したプロパティを登録し、実験してみる。

```groovy
gremlin> :> m = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@5ff209a7
gremlin> :> m.makePropertyKey("string").dataType(String).make()
==>string
gremlin> :> m.makePropertyKey("char").dataType(Character).make()
==>char
gremlin> :> m.makePropertyKey("boolean").dataType(Boolean).make()
==>boolean
gremlin> :> m.makePropertyKey("byte").dataType(Byte).make()
==>byte
gremlin> :> m.makePropertyKey("short").dataType(Short).make()
==>short
gremlin> :> m.makePropertyKey("integer").dataType(Integer).make()
==>integer
gremlin> :> m.makePropertyKey("long").dataType(Long).make()
==>long
gremlin> :> m.makePropertyKey("float").dataType(Float).make()
==>float
gremlin> :> m.makePropertyKey("double").dataType(Double).make()
==>double
gremlin> :> m.makePropertyKey("date").dataType(java.util.Date).make()
==>date
gremlin> :> m.makePropertyKey("geoshape").dataType(Geoshape).make()
==>geoshape
gremlin> :> m.makePropertyKey("uuid").dataType(UUID).make()
==>uuid
gremlin> :> m.commit()
==>null
```

### 異なるデータタイプの登録

例えば、Stringで定義したプロパティにIntegerを入れてみる

```groovy
gremlin> :> g.addV("type").property("string", 999)
==>v[4296]
gremlin> :> s = g.V(4296).values("string).next()
==>999
gremlin> :> s.class
==>class java.lang.String
```

エラーは発生しなかった(意外だ)。しかし、データとしてはStringに変換(toString)されたうえで格納されているようだ。次は逆に、Integerで定義したプロパティにStringを入れてみる。

```groovy
gremlin> :> g.addV("type").property("integer", "abc")
Value [abc] is not an instance of the expected data type for property key [integer] and cannot be converted. Expected: class java.lang.Integer, found: class java.lang.String
Type ':help' or ':h' for help.
Display stack trace? [yN]
gremlin> :> g.addV("type").property("integer", "999")
==>v[4304]
gremlin> :> i = g.V(4304).values("integer").next()
==>999
gremlin> :> i.class
==>class java.lang.Integer
```

文字列`abc`を突っ込もうとしたところ、エラーが発生した(期待通り)。一方、文字列`999`を突っ込んだところ、エラーなく通ってしまった(正直エラーにして欲しいような気もするが)。どうやら数値型に変換されたようだ。

データのシリアライザが他の型からの変換を行うように実装されている。

[IntegerSerializer.java](https://github.com/JanusGraph/janusgraph/blob/d12adfbf083f575fa48860daa37bfbd0e6095369/janusgraph-core/src/main/java/org/janusgraph/graphdb/database/serialize/attribute/IntegerSerializer.java#L65)
[StringSerializer.java](https://github.com/JanusGraph/janusgraph/blob/3b71cd389cd1e9f04ea389962166eeb851683f8e/janusgraph-core/src/main/java/org/janusgraph/graphdb/database/serialize/attribute/StringSerializer.java#L94)

設定で実装のクラスを差し替えたらこの辺の挙動を自分好みにできるかもしれないけど、試していないので本当にできるかは保証しない。

[Datatype and Attribute Serializer Configuration](https://docs.janusgraph.org/v0.3/advanced-topics/serializer/)
[Configuration Reference - attributes.custom *](https://docs.janusgraph.org/basics/configuration-reference/#attributescustom)
[StandardSerializer.java](https://github.com/JanusGraph/janusgraph/blob/b96aa6d26f74d3dc8f7404212bddeefe2d0790b4/janusgraph-core/src/main/java/org/janusgraph/graphdb/database/serialize/StandardSerializer.java#L78)

### Character

```groovy
gremlin> :> g.addV("type").property("char", 'a').next().values("char")
// 注)　長くて複雑な処理のように見えるが、next()までで頂点の追加を行い、values()でそのプロパティを取り出しているだけである
==>a
gremlin> :> g.addV("type").property("char", "a").next().values("char")
==>a
gremlin> :> g.addV("type").property("char", "ab").next().values("char")
Value [ab] is not an instance of the expected data type for property key [char] and cannot be converted. Expected: class java.lang.Character, found: class java.lang.String
Type ':help' or ':h' for help.
Display stack trace? [yN]
gremlin> :> g.addV("type").property("char", "あ").next().values("char")
==>あ
```
文字列型でも1文字なら通る。当然2文字以上は弾かれる。

### Boolean

```groovy
gremlin> :> g.addV("type").property("boolean", true).next().values("boolean")
==>true
gremlin> :> g.addV("type").property("boolean", false).next().values("boolean")
==>false
gremlin> :> g.addV("type").property("boolean", 1).next().values("boolean")
==>true
gremlin> :> g.addV("type").property("boolean", 0).next().values("boolean")
==>false
gremlin> :> g.addV("type").property("boolean", "true").next().values("boolean")
==>true
gremlin> :> g.addV("type").property("boolean", "false").next().values("boolean")
==>false
gremlin> :> g.addV("type").property("boolean", "null").next().values("boolean")
==>false
gremlin> :> g.addV("type").property("boolean", "hoge").next().values("boolean")
==>false
gremlin> :> g.addV("type").property("boolean", 999).next().values("boolean")
Value [999] is not an instance of the expected data type for property key [boolean] and cannot be converted. Expected: class java.lang.Boolean, found: class java.lang.Integer
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

Booleanにキャストできるものは大体何を入れても通るようだ。これはおそらく他の型も同様と思われる。

### Byte/Short/Integer/Long

面倒なのでShortだけ試す。

```groovy
gremlin> :> g.addV("type").property("short", 32767).next().values("short")
==>32767
gremlin> :> g.addV("type").property("short", 32768).next().values("short")
Value [32768] is not an instance of the expected data type for property key [short] and cannot be converted. Expected: class java.lang.Short, found: class java.lang.Integer
Type ':help' or ':h' for help.
Display stack trace? [yN]
gremlin> :> g.addV("type").property("short", -32768).next().values("short")
==>-32768
gremlin> :> g.addV("type").property("short", -32769).next().values("short")
Value [-32769] is not an instance of the expected data type for property key [short] and cannot be converted. Expected: class java.lang.Short, found: class java.lang.Integer
Type ':help' or ':h' for help.
Display stack trace? [yN]
```

ちゃんと境界チェックしてくれる。グッド。

### Float/Double

```groovy
gremlin> :> g.addV("type").property("float", 1.0).next().values("float")
==>1.0
gremlin> :> g.addV("type").property("float", 0.1).next().values("float")
==>0.1
gremlin> :> g.addV("type").property("float", 1.23456789e-12).next().values("float")
==>1.2345679E-12
gremlin> :> g.addV("type").property("double", 1.23456789012345678e-12).next().values("double")
==>1.2345678901234567E-12
```

### Date/Geoshape/UUID

```groovy
gremlin> :> g.addV("type").property("date", null).next().values("date")
gremlin> :> g.addV("type").property("date", new Date(120, 3, 22)).next().values("date")
==>Wed Apr 22 00:00:00 UTC 2020
gremlin> :> g.addV("type").property("geoshape", null).next().values("geoshape")
gremlin> :> g.addV("type").property("geoshape", Geoshape.point(37.97, 23.72)).next().values("geoshape")
==>POINT (23.72 37.97)
gremlin> :> g.addV("type").property("uuid", null).next().values("uuid")
gremlin> :> g.addV("type").property("uuid", UUID.randomUUID()).next().values("uuid")
==>d54a43dd-5c2d-4c78-98de-b9ffee0c7e00
```

`null`でも平然と受け入れるみたい。

## まとめ

- Byte/Short/Integer/Longなどの整数型については値の範囲をチェックしてくれる
- 文字列型と数値型は、変換できる場合は相互に自動変換されて登録される。これは人によっては好ましくない挙動かもしれない
- Date/Geoshape/UUIDなどのオブジェクト型はnullも入れられる

致命的なミスの一部は回避してくれるので、スキーマを定義すべきなのは間違いないが、それだけで十分ということはないので、自分でしっかりとしたValidationコードを書く必要がある。