本文要点
驱动器(Driver)、映射器(Mapper)、DAO、活动记录(Active Record)和存储库(Repository)等 Java 持久化模式对于健壮的数据库交互和应用架构至关重要,每种模式都提供了不同的数据管理方法。
平衡层对于管理复杂性和优化数据流至关重要,每种模式都有自己的优缺点,为了做出明智的架构决策,需要对它们加以考虑。
面向对象编程和面向数据编程之间的选择会影响软件的设计,要注重解决阻抗不匹配并保持两者之间的平衡。
虽然这些模式为 Java 持久化提供了健壮的解决方案,但要想有效地实现,必须仔细考虑数据同步和过度工程等权衡因素。
命令和查询职责分离(Command and Query Responsibility Segregation,CQRS)模式明确分离读取和更新操作,具有显著的性能、可伸缩性和安全性的优势。但是,这种权衡可能会增加应用程序中的复杂性。在不断发展的软件架构领域,高级工程师、架构师和 CTO 都面临着一个长期的挑战,那就是如何为 Java 应用程序设计一个健壮和高效的持久层。精心设计的持久层不仅仅是技术细节,它是支撑应用程序功能、可扩展性和长期可维护性的基石。本文将深入探讨与 Java 持久层相关的设计模式这一复杂领域,着重于明确区分面向对象和面向数据的方法。
在 Java 应用程序中,持久层是连接应用中复杂业务逻辑与底层数据存储(通常是关系型数据库)的桥梁。在这一层中所做出的选择会贯穿整个软件的生命周期,影响其性能、可维护性和适应性。为了应对这一挑战,我们必须掌握 Java 持久化中的两个主要范式。
面向对象与面向数据:处理阻抗不匹配
每当涉及数据库持久化引擎和 Java 应用程序领域时,我们都会面临一个基本的挑战,那就是如何弥合应用程序范式与数据库本身的差异。在这个转换过程中,通常会引入阻抗不匹配(impedance mismatch),这会严重影响应用程序的性能和可维护性。这是一项至关重要的任务,因为当我们将 Java 与任意数据库引擎进行比较的时候,我们面对的是两种完全不同的原理和概念。
一方面是 Java 语言,它是一门拥有继承、多态、封装和丰富类型系统的语言。这些面向对象的概念塑造了我们设计和构建应用程序的方式。它们提供了一个高层次的抽象和结构,帮助我们管理复杂性并有效维护代码。
而另一方面,我们看一下数据库,会发现这是一个由规范化、非规范化、索引和查询优化等概念主导的世界。数据库主要关注如何高效地存储和检索数据,通常将性能作为首要考虑因素。数据库本身并不理解或支持 Java 的面向对象特性,这可能会导致在尝试同步这两个不同的世界时出现阻抗。
图 1:数据库和 Java 编程语言之间的不匹配
我们依靠各种设计模式和架构方式来弥合这一差异,并在 Java 应用程序和数据库之间创建无缝连接。这些模式就像翻译器一样,有助于减少阻抗不匹配的影响,使得这两个世界能够和谐共处。
这些设计模式并不是在重复发明轮子。它们是行之有效的解决方案,在缓解应用程序和数据库范式之间的阻抗不匹配方面被证明是行之有效的,它们包括驱动器模式(Driver Pattern)、映射器模式(Mapper Pattern)、活动记录模式(Active Record Pattern)和存储库模式(Repository Pattern)。
Java 持久化领域的数据模式概览
在本文的这一部分中,我们将深入介绍 Java 持久化中的数据模式,主要关注在应用程序开发和数据库管理中,面向对象编程和面向数据编程的细微差别。这两种编程范式之间的差异在软件设计方案选择方面起着至关重要的作用,我们将会探讨每种方法的利弊权衡。
在这个领域的一端,我们采用的是经典的面向对象编程(Object-Oriented Programming,OOP)范式。受Robert Martin在《代码整洁之道》等图书中所阐述的原则的启发,OOP 非常强调如下几个关键的方面:
隐藏数据,暴露行为:OOP 鼓励封装,即隐藏内部的数据结构,暴露明确定义的行为接口。这种方式通过将数据操作限制在受控的方法中,达到模块化和可维护性的目的。
多态可以将不同的对象视为具有共同特质的对象来处理。在 Java 中,多态是通过方法覆盖和重载实现的,允许对不同的对象类型进行动态和灵活的方法调用。
抽象简化了软件中复杂的概念,这是通过将现实世界的对象建模为类来达成的。在 Java 中,抽象是通过抽象类和接口实现的,这样既能确保行为的一致性,又能允许存在各种具体实现。在这个领域的另一端,我们采用面向数据编程(Data-Oriented Programming,DOP)的原则,该原则是由Yehonathan Sharvit定义的,他是具有二十多年专业知识的资深软件工程师。在处理数据库和数据密集型操作时,这些原则尤为重要。DOP 鼓励采用如下的实践:
将代码(行为)与数据分离:DOP 倡导将数据操作逻辑与数据本身分离。这种分离可以提升数据处理的灵活性和效率。
用通用的数据结构来表示数据:DOP 建议使用通用的数据结构来存储数据,从而实现高效的数据操作和处理,而不是依赖复杂的对象层级。
将数据视为不可变的:数据的不变性是 DOP 的一个关键概念。不可变的数据能够确保数据变更是可控和可预测的,从而使其适合并发处理。
将数据模式与数据表述分离:DOP 鼓励将数据结构(模式)与数据表述分离,这能够使得数据管理具有灵活性和适应性。当进一步深入本节的内容时,我们必须要明白,Java 持久化中数据模式的选择并不是放之四海而皆准的。选择合适的模式取决于应用程序的具体需求、领域的复杂性和性能方面的考虑因素。当我们在数据世界中平衡一致性、可用性和分区容忍性(通常称为“CAP 理论”)时,必须要认识到,没有一种模式能够完美适用于任何场景。
在面向对象和面向数据编程之间找到恰当的平衡是一项持续的工作。我们将会探讨各种设计模式,包括驱动器模式、数据映射器、DAO、活动记录和存储库,以理解它们如何适应这些范式,以及如何弥合应用程序逻辑与数据库交互之间的差异。通过这些探索,我们的目标是优化软件架构的性能、可维护性和可扩展性,同时认识到这一过程是不断发展的。
图 2:数据设计模式与编程风格的距离
要加深对 Java 中面向数据编程的理解,你还可以阅读 Brian Goetz 的文章“Java中的面向数据编程”。
弥合差距:从数据库到对象的模式
在探索 Java 持久化设计模式时,我们将会从数据库本身的核心开始,逐渐过渡到面向对象编程这一端。这种方式允许我们首先深入研究与数据库直接交互的模式,突出强调面向数据的原则和如何处理原始信息。随着学习的深入,我们将会把重点放到面向对象编程上,将数据转化为具体应用程序中的实体。从贴近数据库的模式开始,一直介绍到符合面向对象范式的模式,在这个过程中,我们会理解如何弥合数据管理与应用程序逻辑之间的差异,从而创建健壮而高效的 Java 应用程序。接下来,让我们揭示将软件开发领域中这两个基础方面无缝连接起来的模式把。
驱动器模式
首先,我们会讨论驱动器模式及其在数据库通信中的作用。这种模式更接近数据库,为面向数据的编程提供了一个独特的视角,展示了它所提供的灵活性。
驱动器模式主要负责与数据库建立连接并进行通信。在很多场景中,这种模式在数据库层更为流畅,你可以在各种样例和框架中看到它的实现,比如关系型数据库的 JDBC 驱动或 MongoDB 和 Cassandra 等 NoSQL 数据库的通信层。
如下的代码片段提供了一个基于 Java 和 JDBC 进行数据库通信,以实现驱动器模式的简单样例。该片段演示了如何从数据库表中提取数据,并展示了与面向数据编程相关的不可变性:
在这段代码中,ResultSet
的行为类似于一个只读的 Map,提供了从数据库查询结果中访问数据的 getter 方法。这种方式符合面向数据编程的原则,强调数据的不可变性。驱动器模式和这种面向数据的方式提供了处理数据的灵活性,允许我们从数据的角度将其作为一等实体来进行处理。但是,这种灵活性也需要在将数据转换为应用程序具体的实体时引入额外的代码,从而可能导致复杂性的增加,并有可能引入缺陷。
驱动器模式说明,在应用程序架构中,我们越接近数据库就越能与作为原始信息的数据进行交互,这对于特定场景会很有助益。不过,它凸显了在将数据从数据库转换到应用层时,深思熟虑的设计和抽象的重要性,这能够显著降低复杂性和潜在的错误。
在驱动器模式的场景中,我们可以初步看到面向数据编程的身影,它为处理原始数据提供了极大的灵活性。但是,这往往需要将数据转换为对我们的业务领域有意义的表述形式,尤其是在处理领域驱动设计(Domain-Driven Design,DDD)的时候更是如此。我们引入了“数据映射器”模式来简化这种转换,它是企业应用架构模式中的一个强大工具。
数据映射器模式
数据映射器(Data Mapper) 是位于对象和数据库之间一个重要的层,确保了对象和数据库之间以及与映射器本身的独立性。它提供了一个中心化的方式来桥接面向数据和面向对象的编程范式。不过,值得注意的是,尽管这种模式简化了数据到对象的转换过程,但也带来了阻抗不匹配的可能性。
在多种框架中都可以看到数据映射器模式的实现,比如Jakarta Persistence(以前叫做 JPA)。Jakarta Persistence 允许使用注解映射实体,以创建数据库和对象之间的无缝连接。如下的代码片段演示了如何使用 Jakarta Persistence 注解映射“Person
”实体:
除此之外,如果不喜欢注解,还可以使用其他方法。例如,Spring JDBC模板提供了一种灵活的方式。我们可以创建一个自定义的“PersonRowMapper
”类,将数据库行映射为“Person
”实体,如下所示:
数据映射器模式并不局限于关系型数据库。我们还可以看到基于注解或手动数据-对象转换的方式在 NoSQL 数据库领域中也有对应的实现。这种多样性使得数据映射器模式成为处理各种数据库技术的宝贵资产,同时又能保持数据和领域模型之间的明确分离。
数据访问对象(DAO)
映射器模式的确提供了一种有效的方式,可以中心化地处理数据库和实体表述之间的转换,从而在代码测试和维护方面带来了巨大的优势。此外,它还可以将数据库操作整合到一个专门的层中。**数据访问对象(DAO)**模式是这一类别中最著名的模式之一,它专门用来提供数据操作,同时使得应用程序能够免受复杂的数据库细节的干扰。
DAO 是作为一个关键组件,抽象和封装了与数据源的所有交互。它可以有效管理与数据源的连接,以便于检索和存储数据,同时保持数据库与应用程序业务逻辑之间的明确分离。这种分离能够使架构更健壮、更易于维护。
DAO 模式的显著优势之一在于,它在应用程序的两个部分之间实现了严格的分离,这两个部分并不需要相互了解。这种分离使它们能够独立且快速地发展。当业务逻辑发生变化时,它可以依赖于一致的 DAO 接口,而持久化逻辑的修改不会影响 DAO 的客户端。
在如下展示的代码片段中,可以看到“Person
”实体的 DAO 接口示例。该接口抽象了数据访问操作,使其成为 Java 应用程序中管理数据的强大工具:
值得注意的是,尽管 DAO 对数据访问进行了抽象和封装,但它隐式地依赖于映射器模式来处理数据库和实体表述之间的转换。因此,模式之间的这种相互作用可能会引入阻抗不匹配,这是数据库操作中需要解决的一个挑战。DAO 模式的用途很广泛,可以与各种数据库框架协同实现,包括 SQL 和 NoSQL 技术。例如,在使用 Java Persistence 的时候,可以创建一个“PersonDAO
”接口的实现,以简化对“Person
”实体的数据库操作。该实现会有效利用映射器模式,在数据库和应用程序的领域实体之间架起一座桥梁。
该代码片段是PersonDAO
的简化版 Jakarta Persistence 实现。它演示了如何使用 Jakarta Persistence API 来与数据库进行交互并执行常见的数据访问操作。
findById
方法通过唯一标识符检索 Person 实体,findAll
方法则检索数据库中所有 Person 实体的列表。这些方法为 Java 应用程序与底层数据库之间的无缝集成奠定了基础,体现了 Jakarta Persistence 在数据访问方面的强大功能和简便性。
活动记录模式
接下来,我们遇到的是持久化设计模式中的**活动记录(Active Record)**模式。这种模式允许在继承的基础上直接实现与数据库的集成,从而赋予实体自我管理数据库操作的“超能力”。这种方法将操作整合到了实体本身之中,从而简化了数据库集成。不过,这种方式也是有一定代价的,包括紧耦合以及可能违反单一职责原则。
活动记录模式在 Ruby 社区很受欢迎,并且进入了 Java 领域,这主要是通过基于 Panache 项目的Quarkus框架实现的。Panache 通过实现活动记录模式简化了 Java 中的数据库集成,使实体无需单独的数据访问层即可执行数据库操作。
下面是一个使用 Quarkus Panache 和Person
实体实现活动记录的样例:
在这段代码中,Person
实体扩展了PanacheEntity
,它是 Quarkus Panache 项目的一部分。因此,Person
实体继承了用于数据库操作的persist()
、listAll()
和findById()
等方法。这意味着Person
实体可以自行管理与数据库的交互。虽然活动记录模式简化了数据库操作,并且不再需要单独的数据访问层,但必须要考虑其中的权衡。实体和数据库之间的紧耦合以及可能违反单一职责原则,都是决定采用这一模式时的考量因素。请根据应用程序的具体需求和愿意接受的考量因素,活动记录模式可以成为简化 Java 数据库集成的强大工具。
存储库模式
我们继续探讨 Java 持久化模式,接下来就是存储库(Repository)模式,它代表了一种重要的转变,即更加倾向于面向领域的方式。存储库位于领域和数据映射层之间,引入了一个内存领域的集合,与应用程序的通用语言保持一致。这种模式强调特定领域的语义,有助于实现与数据进行更自然和更具表现力的交互。
存储库与前文讨论的 DAO 之间的主要区别在于对领域的关注。DAO 专注于数据库集成(比如像插入和更新这样的操作),而存储库则引入了更多与领域语言密切相关的声明式方法。这种抽象化允许在语义上与领域保持一致,同时还能管理应用程序与数据库之间的距离。
在 Java 生态系统中,有多个框架和规范支持存储库模式。具有代表性的包括Spring Data、Micronaut Data和Jakarta Data规范。
我们看一下使用 Jakarta Persistence 注解实现Person
类的过程,并探索如何利用 Jakarta Data 来实现存储库模式:
在这段代码中,Person
实体使用了 Jakarta Persistence 注解,我们还引入了一个People
接口,该接口扩展了 Jakarta Data 提供的CrudRepository
接口。这个存储库接口使用了存储库模式,提供了声明式的方法,如save
、findAll
和findById
。这些方法提供了一种更加面向领域、更具表现力、语义更一致的数据库交互方式,有助于提高代码库的清晰度和可维护性。我们将继续探索以领域为中心的存储库和不断演进的Jakarta Data规范,考虑一个涉及Car
和Garage
存储库的实际样例。在这个场景中,我们的目标是创建一个与领域高度一致的自定义存储库,并利用动作注解来表达存储库中汽车停放和取消停放的操作。
如下是阐述这一概念的代码:
这种模式提供了一种极具表现力且以领域为中心的方式,以实现与Garage
存储库的交互。它将存储库的方法与领域语言和动作紧密结合在了一起,使代码更加直观和具有自描述性。注解(如@Save
和@Delete
)的使用明确了这些方法背后的意图,简化了 Java 应用程序中领域驱动数据访问层的开发。虽然存储库模式引入了以领域为中心的视角,这是难能可贵的,但平衡语义的清晰性以及管理领域与数据库之间的距离所面临的挑战是至关重要的。根据项目的具体要求,存储库模式可以成为 Java 应用程序中一个强大的工具,用来创建更具表现力和领域驱动的数据访问层。
重要的面向数据模式概览
在进一步深入研究 Java 持久化模式之前,我们来概览一下重要的模式,它们包括驱动器、映射器、DAO、活动记录和存储库模式。该总结概述了这些模式的优缺点,为我们后续的探索奠定了基础。这是了解这些模式如何影响 Java 应用程序并指导我们学习更高级概念的第一步。
实际上,从数据编程过渡到更加以领域为中心的方法时,我们经常会发现需要引入额外的层来调解应用程序不同部分之间的通信。每种模式都会占据一个不同的层,这种分层架构引入了一个抽象层次,将应用程序的核心逻辑与数据库操作分离开来。
图 3:面向数据编程和以领域为中心的方法会有不同的层
数据传输对象(DTO)
接下来,我们介绍一种被广泛使用的通用模式,即数据传输对象(DTO)。这种模式有多种用途,包括跨不同的层实现无缝的数据移动,例如,在 RESTful API 中提取数据进行 JSON 表述的时候。此外,DTO 还可以将实体与数据库模式隔离开来,实现实体与各种数据库模型之间关系的透明化。
这种适应性允许应用程序将多种数据库作为潜在的目标,而不影响核心的实体结构。以上只是 DTO 众多使用场景中的两个,它们展示了 DTO 的灵活性。
图 4:在 Java 应用程序中使用 DTO 的两个示例
不过,必须注意的是,虽然 DTO 有很多优点,但它们需要对数据转换进行细致的管理,以确保各层之间的正确隔离。DTO 的使用带来了在应用程序的不同组成部分之间保持一致性和连贯性的挑战,而这正是成功实施 DTO 的重要方面。
命令和查询职责分离(CQRS)
我们已经探索了分层和数据传输对象(DTO)的重要性,现在我们来看一下命令和查询职责分离(CQRS)模式。CQRS 是一种强大的架构策略,它将数据存储中的读取和更新操作分离了开来。值得注意的是,CQRS 的应用可以极大地完善架构中对 DTO 的使用。
在应用程序中实施 CQRS 模式可以带来很多好处,包括最大限度地提高性能、可扩展性和安全性。我们可以使用 DTO 有效地管理 CQRS 架构中读取/写入双方之间的数据传输。这可以确保数据在这些分离的责任之间进行恰当的格式化和转换。
对精通 NoSQL 数据库的人来说,可能已经非常熟悉 CQRS 的概念了。NoSQL 数据库通常采用查询驱动的建模方法,在这种方法中,数据针对检索操作进行了优化,而非针对更新操作。在这种情况下,CQRS 的读取/写入操作分离与数据库的原生行为完美契合。
不过,在使用 CQRS 时,必须要对其有细致的了解。CQRS 在提供优势的同时,也带来了一定的复杂性,因此在采用时应根据应用程序的具体要求进行仔细的权衡。它的一些潜在的缺点包括:
增加复杂性:实施 CQRS 会引入额外的分层和关注点分离,从而增加整个系统架构的复杂性。这种复杂性可能会影响开发时间、调试和开发团队的学习曲线。
数据同步的难题:保持系统读取/写入两端的一致性是一项挑战。由于更新与读取操作是分隔开的,要确保为用户提供同步和最新的视图,可能需要慎重的考虑和额外的机制。
过度工程的可能性:在比较简单的应用中,引入 CQRS 可能需要重新修正原有的系统,并可能导致过度设计。关键是要评估增加的复杂性是否能带来效益,尤其是在有直接数据访问需求的项目中。虽然 CQRS 有一定的优势,但也会带来一定的问题,因此在采用它的时候,应该根据应用程序的具体要求进行仔细权衡。DTO 和 CQRS 之间的协同确实可以提高应用程序架构中的数据传输效率。不过,很重要的一点在于,要认识到收益与挑战是并存的,有必要对系统的复杂性、可维护性和开发速度的整体影响进行慎重的评估。
将 DTO 与 CQRS 结合起来,可以让我们在应用程序架构中高效地管理数据传输。如下图所示,通过保持读取/写入操作之间的明确分离并使用 DTO 作为媒介,我们可以享受 CQRS 在性能、可扩展性和安全性方面的优势,同时能够无缝适应查询驱动的 NoSQL 环境:
结论
在探索 Java 持久性模式的过程中,我们介绍了各种用于满足特定应用程序需求和架构目标的策略。驱动器、映射器、DAO、活动记录和存储库等模式为 Java 应用程序中的数据管理提供了重要的构建基块。它们强调了在各层之间取得适当平衡的重要性,以及在关注潜在的性能影响的同时实现结构化方法的重要性。
数据传输对象(DTO)是一种的通用工具,可实现各层之间无缝的数据传输,并适应各种数据模型。不过,使用这些工具需要谨慎管理数据转换,以确保各应用组件之间的统一性。
最后,我们简单介绍了命令和查询职责分离(CQRS),这是一种将读取和更新操作分开的模式。CQRS 的实现有望带来强大的性能、可扩展性和安全性优势,尤其是在查询驱动建模(如 NoSQL 数据库)占据主导地位的环境中。
这些模式是设计 Java 应用程序的基础,可准确满足独特的要求和业务目标。作为开发人员,了解这些模式的优势和局限性有助于我们做出明智的架构决策,确保我们的应用程序不仅高效,而且能够确保具备健壮性和快速反应的能力。
原文链接:
评论