返回介绍

7.4 将领域建模为图

发布于 2025-04-22 19:57:19 字数 4943 浏览 0 评论 0 收藏

对于 Neo4j 这样的图数据库,在第 1 章中所描述的领域模型就很合适,如图 7-3 所示。为了进行一些更为高级的图操作,我们对其进行了进一步的规范化,并添加了额外的一些关系来对模型进行丰富。

0703

图 7-3 作为图的领域模型

这里所列出的示例代码并不完整,但包含了理解映射理念所必需的信息。参见示例源码库中的 Neo4j 工程可以了解全貌。

在示例 7-4 中,AbstractEntity 会作为超类,它包含了一个相同的 id 域(如前文所述,这个类具有 @GraphId 注解,同时类中包含了 equals(...) 和 hashCode() 方法)。在简单的映射模式下需要有 id 的注解,因为这是保持节点或关系的 id 能够存储到实体的唯一方式。实体可以根据其 id 进行加载,这是通过 Neo4jTemplate.findOne() 方法实现的,在 GraphRepository 中有类似的方法。

示例 7-4 基础领域类

c0704

最简单的映射类只需使用 @NodeEntity,以便 Spring Data Neo4j 的映射基础设施能够找到它。它可以包含任意数量的原始域,它们将会被视为节点属性。原始类型会直接进行映射。借助所提供的 Spring 转换器(converter),能够将 Neo4j 所不支持的类型转换为等价的原始类型展现形式。对 Enum 和 Date 类型的转换器是在类库中内置的。

在 Country 中,有两个简单字符串类型的域,如示例 7-5 所示。域 code 代表一个唯一的“业务”键,并用 @Indexed(unique=true) 进行标示,这样就会启用内置的唯一索引设施;这是通过 Neo4jTemplate.getOrCreateNode() 进行暴露的。在 Neo4jTemplate 中有多种方法来访问 Neo4j 的索引,我们可以使用 Neo4jTemplate.lookup() 根据索引键来查找实体。

示例 7-5 简单实体 Country

c0705

Customer 存储为节点;它们的唯一键是 emailAddress。在这里,我们第一次看到了对其他对象的引用(在本例中,也就是 Address),在图中代表着一个关系。所以,单个引用或集合形式引用的域都会导致在更新的时候创建关系,或者是在访问时进行导航使用。

如示例 7-6 所示,引用的域可以使用 @RelatedTo,这个注解会说明它们是引用域,我们同时可以设置像关系类型(在本例中,就是“ADDRESS”)这样的属性。如果没有提供类型的话,它默认就是域的名字。默认情况下,关系指向被引用的对象(Direction.OUTGOING),可以通过注解来指明相反的方向;对于双向引用来讲,这尤为重要,这种情况下它应该被映射为单个关系。

示例 7-6 Customer 与其 Address 产生了关联

c0706

Address 同样也很简单。示例 7-7 展现了 country 引用域并不需要添加注解 - 对于输出型的关系,它会使用域的名字作为关系类型。Customer 与 Address 的关系在这个映射中没有表现出来,因为对于我们的用例来说没有这样的必要。

示例 7-7 Address 与 Country 之间的关联

c0707

Product 有一个唯一的名字并且展现了非原始类型域的使用方法;price 会使用 Spring 的转换器将其转化为原始类型的表现形式。对于自定义的类型(如值对象),可以在应用上下文中注册自己的转换器。

域 description 添加了索引,从而允许全文搜索。必须明确地给索引命名,因为它使用了与默认精确索引不同的配置。现在可以调用诸如 neo4jTemplate.lookup ("search","description: Mac*") 这样的方式来查找产品,它会接收 Lucene 的查询字符串。

为了使用更为有趣的图操作,我们添加了 Tag 实体,并且在 Product 中与其进行关联。这些标签可以用来查找类似的产品、提供推荐或分析购买行为。

要处理实体中的动态属性(任意键值对形成的 map),在 Spring Data Neo4j 中有一个专门的类。我们决定不直接处理 map,因为它们自带的一些额外语义并不适合我们的上下文。目前,DynamicProperties 转化为节点的属性并使用加前缀的名字进行分割,如示例 7-8 所示。

示例 7-8 具有标签的产品并且包含动态属性

c0708

Tag 的唯一不同之处在于其 Object value 属性。这个属性将会根据运行时的值转换为 Neo4j 可以存储的原始值。如示例 7-9 所示,@GraphProperty 注解允许进行一些个性化的存储(比如,所使用的属性名或指明图中的目标原始类型)。

示例 7-9 很简单的 Tag

c0709

我们所遇到的第一个 @RelationshipEntity 是之前领域模型中所不存在的新概念,但是在网站上它却是广为大家所熟知的。为了进行一些更为有趣的图操作,在 Customer 和 Product 之间添加了 Rating 关系。这个实体使用了 @RelationshipEntity 注解来标识这一点。除了两个基本的域来持有 Rating 的 stars 和 comment 以外,还可以看到它包含了描述关系开始和结束的域,它们分别添加了对应的注解,如示例 7-10 所示。

示例 7-10 Customer 与 Product 之间的 Rating

c0710

关系实体可以作为普通的 POJO 类来进行创建,这个类需要提供其开始和结束端点并通过 Neo4jTemplate.save() 进行保存。在示例 7-11 中,通过 Order 展现了这些实体如何作为映射的一部分进行检索。在关于图操作更深入的讨论中(参见 7.7.3 小节“利用类似的兴趣(协同过滤)),借助 Neo4jTemplate.query 的 Cypher 查询以及 Repository 的查找方法,我们将会看到如何使用这些关系。

Order 是目前为止连接最多的实体,它位于领域的中央。如示例 7-11 所示,在对 Customer 的关系中,对于双向关联且共用相同关系的情况下,我们展示了反向的 Direction.INCOMING。

对不同类型的地址(派送和账单)进行建模的最简单方式就是使用不同的关系类型 - 在本例中,只是依赖不同的域名即可。注意,一个地址对象/节点可以用在多个地方,如某位顾客的派送和账单地址,某个地址甚至可能被多个顾客共享(如一个家庭)。在实践之中,图会比关系型数据库更为标准化,这种对副本的移除会带来一些好处,这包括了存储的方面也包括了运行趣味查询的能力。

示例 7-11 Order,领域的核心

c0711

LineItem 并没有建模为节点,而是作为 Order 和 Product 之间的关系。LineItem 并没有自己的标识,只有当它的两个端点存在的时候它才能存在,在这里通过 order 和 product 域来进行引用。在这个模型中,LineItem 只包含 quantity 属性,但是在其他的用户场景中,它还可以包含其他的属性。

Order 和 LineItem 中,有意思的地方在于 @RelatedToVia 和 @Fetch 注解,我们对其稍作介绍。域 lineItems 上的注解类似于 @RelatedTo,它只是用在关系实体的引用上。我们可以指明自定义的的类型和方向。所指定的类型将会覆盖 @RelationshipEntity 所提供的类型,如示例 7-12 所示。

示例 7-12 LineItem 是一个关系

c0712

现在我们该看一下图映射很重要的一个方面:抓取声明(fetch declaration)。从 JPA 可以知道,这是很棘手的问题。如今在 Spring Data Neo4j 中,通过默认不抓取关联的实体会让事情变得很简单。

因为简单映射模式下需要将图中的数据复制到对象之中,所以对于抓取的深度要特别小心;否则的话,稍有不慎就会将整个图加载在内存之中,因为图结构经常是环形的。这就是为何默认的情况下会以很浅层级的方式加载关联的实体。@Fetch 注解用来声明对应的域马上进行完整的加载。其实可以事后通过 template.fetch(entity.field) 加载它们。这可以用于单关系(一对一)和多关系(一对多)的域。

在 Order 之中,LineItems 默认会被抓取出来,因为当加载 Order 时,它们在大多数场景下都是很重要的。对于 LineItem 来说,Product 也是立即加载的,这样它就直接可用了。根据用例,建模的方式可能会稍有不同。现在我们已经创建了领域类,该把它们的数据存储到图中了。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。