模式和数据建模
每个JanusGraph图都有一个由其中使用的边标签、属性键和顶点标签组成的Schema。JanusGraph Schema可以显式或隐式定义。建议用户在应用程序开发期间显式定义图Schema。显式定义的Schema是健壮的图应用程序的重要组成部分,并大大提高了协作软件开发效率。请注意,JanusGraph Schema可以随着时间的推移而演变,而不会中断正常的数据库操作。扩展Schema不会减慢查询响应速度,也不需要数据库停机。
Schema类型——即边标签、属性键或顶点标签——在图中的元素首次创建时被分配给它们——即边、属性或顶点。一旦分配,特定元素的Schema类型不能更改。这确保了一个稳定且易于理解的类型系统。
除了本节中解释的Schema定义选项之外,Schema类型还提供性能调优选项,这些选项在高级Schema中讨论。
显示Schema信息
在管理API中,有方法可以查看图Schema的特定元素。这些方法是mgmt.printIndexes()、mgmt.printPropertyKeys()、mgmt.printVertexLabels()和mgmt.printEdgeLabels()。还有一个名为printSchema()的方法,可以显示所有这些方法的组合输出。
mgmt = graph.openManagement()
mgmt.printSchema()
定义边标签
连接两个顶点的每条边都有一个标签,用于定义关系语义。例如,顶点 A 和 B 之间标记为 friend 的边编码了两个人之间的友谊。
要定义边标签,请在开放的图或管理事务上调用 makeEdgeLabel(String),并将边标签的名称作为参数。边标签名称在图中必须是唯一的。此方法返回一个边标签构建器,允许定义其多重性。边标签的 多重性 定义了该标签所有边的多重性约束,即一对顶点之间边的最大数量。JanusGraph 识别以下多重性设置。
边标签多重性
- MULTI:允许任何一对顶点之间存在多个相同标签的边。换句话说,图对于此类边标签而言是多重图。对边多重性没有约束。
- SIMPLE:允许任何一对顶点之间最多只有一条此类标签的边。换句话说,图对于该标签而言是简单图。确保给定标签和顶点对的边是唯一的。
- MANY2ONE:允许图中任何顶点上最多有一条此类标签的出边,但对入边没有限制。边标签
mother是MANY2ONE多重性的一个例子,因为每个人最多只有一个母亲,但母亲可以有多个孩子。 - ONE2MANY:允许图中任何顶点上最多有一条此类标签的入边,但对出边没有限制。边标签
winnerOf是ONE2MANY多重性的一个例子,因为每个比赛最多只有一个人获胜,但一个人可以赢得多个比赛。 - ONE2ONE:允许图中任何顶点上最多有一条此类标签的入边和一条出边。边标签marriedTo是ONE2ONE多重性的一个例子,因为一个人只与另一个人结婚。
默认多重性为 MULTI。边标签的定义通过调用构建器上的 make() 方法完成,该方法返回定义的边标签,如以下示例所示。
mgmt = graph.openManagement()
follow = mgmt.makeEdgeLabel('follow').multiplicity(MULTI).make()
mother = mgmt.makeEdgeLabel('mother').multiplicity(MANY2ONE).make()
mgmt.commit()
定义属性键
顶点和边上的属性是键值对。例如,属性 name='Daniel' 具有键 name 和值 'Daniel'。属性键是 JanusGraph Schema 的一部分,可以约束允许的数据类型和值的基数。
要定义属性键,请在开放的图或管理事务上调用 makePropertyKey(String),并将属性键的名称作为参数。属性键名称在图中必须是唯一的,建议在属性名称中避免空格或特殊字符。此方法返回一个属性键的构建器。
注意
在创建属性键时,考虑同时创建图索引以获得更好的性能,请参阅索引性能。
属性键数据类型
使用 dataType(Class) 定义属性键的数据类型。JanusGraph 将强制所有与该键关联的值都具有配置的数据类型,从而确保添加到图中的数据有效。例如,可以定义 name 键具有 String 数据类型。请注意,不支持原始类型。请使用对应的包装类,例如 Integer 而不是 int。
将数据类型定义为 Object.class 以允许任何(可序列化的)值与键关联。但是,鼓励尽可能使用具体数据类型。配置的数据类型必须是具体类,而不是接口或抽象类。JanusGraph 强制执行类相等性,因此不允许添加配置数据类型的子类。
JanusGraph 原生支持以下数据类型。
| 名称 | 描述 |
|---|---|
| 字符串 | 字符序列 |
| 字符 | 单个字符 |
| 布尔值 | 真或假 |
| 字节 | 字节值 |
| 短整型 | 短整型值 |
| 整数 | 整型值 |
| 长整数 | 长整型值 |
| 浮点型 | 4字节浮点数 |
| 双精度浮点数 | 8字节浮点数 |
| 日期 | 特定时间点(java.util.Date) |
| 地理形状 | 地理形状,如点、圆或盒 |
| UUID | 通用唯一标识符(java.util.UUID) |
属性键基数
使用 cardinality(Cardinality) 来定义在任何给定顶点上与键关联的值的允许基数。
- SINGLE:对于此类键,每个元素最多允许一个值。换句话说,图中的所有元素的键→值映射都是唯一的。属性键
birthDate就是具有SINGLE基数的一个例子,因为每个人都只有一个出生日期。 - LIST:允许此类键的每个元素具有任意数量的值。换句话说,该键与一个值列表相关联,允许重复值。假设我们将传感器建模为图中的顶点,属性键
sensorReading是一个具有 LIST 基数的例子,以允许记录大量(可能重复的)传感器读数。 - SET:允许每个元素有多个值但没有重复值。换句话说,该键与一组值相关联。如果我们想捕捉一个人的所有姓名(包括昵称、娘家姓等),则属性键
name具有 SET 基数。
默认基数设置为 SINGLE。请注意,边和属性上使用的属性键具有基数 SINGLE。不支持为边或属性上的单个键附加多个值。
mgmt = graph.openManagement()
birthDate = mgmt.makePropertyKey('birthDate').dataType(Long.class).cardinality(Cardinality.SINGLE).make()
name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make()
sensorReading = mgmt.makePropertyKey('sensorReading').dataType(Double.class).cardinality(Cardinality.LIST).make()
mgmt.commit()
关系类型
边标签和属性键统称为关系类型。关系类型名称在图中必须是唯一的,这意味着属性键和边标签不能同名。JanusGraph API 中有方法可以查询关系类型是否存在或检索关系类型,包括属性键和边标签。
mgmt = graph.openManagement()
if (mgmt.containsRelationType('name'))
name = mgmt.getPropertyKey('name')
mgmt.getRelationTypes(EdgeLabel.class)
mgmt.commit()
定义顶点标签
与边一样,顶点也有标签。与边标签不同,顶点标签是可选的。顶点标签用于区分不同类型的顶点,例如用户顶点和产品顶点。
尽管在概念和数据模型层面,标签是可选的,但 JanusGraph 会将所有顶点分配一个标签,作为内部实现细节。由 addVertex 方法创建的顶点使用 JanusGraph 的默认标签。
要创建标签,请在开放的图或管理事务上调用 makeVertexLabel(String).make(),并将顶点标签的名称作为参数。顶点标签名称在图中必须是唯一的。
mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
mgmt.commit()
// Create a labeled vertex
person = graph.addVertex(label, 'person')
// Create an unlabeled vertex
v = graph.addVertex()
graph.tx().commit()
自动Schema创建器
如果一个边标签、属性键或顶点标签没有被显式定义,那么在它首次被用于添加边、顶点或设置属性时,它将被隐式定义。为 JanusGraph 图配置的 DefaultSchemaMaker 定义了这些类型。
默认情况下,隐式创建的边标签具有 MULTI 多重性,隐式创建的属性键具有 SINGLE 基数。隐式创建的属性键的数据类型在 JanusGraph 原生支持的情况下会被推断。当且仅当给定值不是任何 Native JanusGraph Data Types 时,才使用 Object.class。用户可以通过实现和注册自己的 DefaultSchemaMaker 来控制自动 Schema 元素创建。
当为顶点属性定义一个不同于 SINGLE 的基数时,该基数应该用于第一次查询中顶点属性的所有值(即定义新顶点属性键的查询)。
强烈建议显式定义所有Schema元素,并通过在JanusGraph图配置中设置schema.default=none来禁用自动Schema创建。
更改Schema元素
一旦边标签、属性键或顶点标签的定义被提交到图中,就不能再更改。但是,Schema元素的名称可以通过 JanusGraphManagement.changeName(JanusGraphSchemaElement, String) 进行更改,如下例所示,其中属性键 place 被重命名为 location。
mgmt = graph.openManagement()
place = mgmt.getPropertyKey('place')
mgmt.changeName(place, 'location')
mgmt.commit()
请注意,Schema名称更改可能不会立即在当前运行的事务和集群中的其他 JanusGraph 图实例中可见。虽然 Schema 名称更改通过存储后端通知所有 JanusGraph 实例,但 Schema 更改可能需要一段时间才能生效,并且在某些故障情况下(例如网络分区)如果与重命名同时发生,可能需要实例重启。因此,用户必须确保以下任一条件成立:
- 重命名的标签或键当前未被主动使用(即未被写入或读取),并且在所有 JanusGraph 实例都知道名称更改之前不会被使用。
- 运行中的事务积极适应短暂的中间期,在此期间旧名称或新名称根据特定的 JanusGraph 实例和名称更改通知状态而有效。例如,这意味着事务可以同时查询两个名称。
如果需要重新定义现有的Schema类型,建议将此类型的名称更改为当前未(且永远不会)使用的名称。之后,可以使用原始名称定义新的标签或键,从而有效地替换旧的。但是,请注意,这不会影响先前使用现有类型写入的顶点、边或属性。不支持在线重新定义现有图元素,必须通过批量图转换来完成。
请注意,如果您更改了作为混合索引一部分的属性名称,则不应在同一索引中重复使用该属性名称。例如,以下代码将失败。
name = mgmt.makePropertyKey("name").dataType(String.class).make()
mgmt.buildIndex("nameIndex", Vertex.class).addKey(name).buildMixedIndex("search")
mgmt.commit()
mgmt = graph.openManagement()
mgmt.changeName(mgmt.getPropertyKey("name"), "oldName");
name = mgmt.makePropertyKey("name").dataType(String.class).make()
mgmt.addIndexKey(mgmt.getGraphIndex("nameIndex"), name)
mgmt.commit()
// the following query will throw an exception
graph.traversal().V().has("name", textContains("value")).hasNext()
原因是,当您调用 mgmt.changeName 时,JanusGraph 不会尝试更改存储在混合索引后端中的字段名称。相反,JanusGraph 维护属性名称和索引字段名称之间的双重映射。如果您使用旧名称创建新索引并将其添加到同一索引,则会发生冲突,因为 JanusGraph 无法确定索引字段应该映射到哪个属性。
Schema约束
Schema 的定义允许用户配置显式属性和连接约束。属性可以绑定到特定的顶点标签和/或边标签。此外,连接约束允许用户显式定义哪些两个顶点标签可以通过边标签连接。这些约束可用于确保图与给定的域模型匹配。例如,对于众神之图,一个 god 可以是另一个 god 的兄弟,但不能是 monster 的兄弟,一个 god 可以有一个属性 age,但 location 不能有一个属性 age。这些约束默认是禁用的。
通过设置 schema.constraints=true 来启用这些 Schema 约束。此设置依赖于 schema.default 设置。如果配置 schema.default 设置为 none,则在违反 Schema 约束时会抛出 IllegalArgumentException。如果 schema.default 未设置为 none,则会自动创建 Schema 约束,但不会抛出异常。激活 Schema 约束对现有数据没有影响,因为这些 Schema 约束仅在插入过程中应用。因此,这些约束完全不影响数据读取。
可以使用 JanusGraphManagement.addProperties(VertexLabel, PropertyKey...) 将多个属性绑定到顶点,例如
mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make()
birthDate = mgmt.makePropertyKey('birthDate').dataType(Long.class).cardinality(Cardinality.SINGLE).make()
mgmt.addProperties(person, name, birthDate)
mgmt.commit()
可以使用 JanusGraphManagement.addProperties(EdgeLabel, PropertyKey...) 将多个属性绑定到边,例如
mgmt = graph.openManagement()
follow = mgmt.makeEdgeLabel('follow').multiplicity(MULTI).make()
name = mgmt.makePropertyKey('name').dataType(String.class).cardinality(Cardinality.SET).make()
mgmt.addProperties(follow, name)
mgmt.commit()
可以使用 JanusGraphManagement.addConnection(EdgeLabel, VertexLabel out, VertexLabel in) 在出、入和边之间定义连接,例如
mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
company = mgmt.makeVertexLabel('company').make()
works = mgmt.makeEdgeLabel('works').multiplicity(MULTI).make()
mgmt.addConnection(works, person, company)
mgmt.commit()