在Java中使用关系数据库时,问题之一是由于数据源的表格性质,它们无法转换对象关系(例如组合)。这意味着作为开发人员,我们通常倾向于拥有一个中间层,该中间层负责抽象数据源的数据组织。这称为ORM或对象关系映射。在Spring的生态系统中,事实上的标准是Hibernate,但是尚未提供新的非阻塞Spring R2DBC API。
坦白地说,您不需要Hibernate进行对象映射,因为R2DBC易于使用,您可以自己完成。而且,这是一种极大的乐趣,特别是因为我们(Java开发人员)倾向于依赖于不是软件设计最新技术的工具。
这篇文章着重于从Postgresql获取组合实体(使用INNER JOIN)并使用具有自定义Spring ReactiveCrudRepository扩展名的Data Mapper模式(如EAA的P中定义)将其映射的问题。
问题 在面向对象的程序设计中,我们基于聚合,关联或组成来处理彼此之间拥有丰富关系的对象。想象一下,您在一个任务管理应用程序上工作,其中每个任务实体都拥有对项目实体的引用。或者,另一种情况是工作应用程序,其中工作实体指向相应的雇主对象。
但是,在关系数据库中,尽管有其名称,但数据是以表格形式组织的。这意味着我们需要在数据库和业务逻辑之间有一个中间层。这种机制称为对象关系映射(ORM),它允许以与数据实体在数据源中存储方式无关的方式访问数据实体。在Java中,事实上选择的是Hibernate,而作为Spring开发人员,我们在关系数据库(例如MySQL,Postgre等)中经常使用它。
但是,当您使用非阻塞式Webflux API和R2DBC来处理关系数据库时,您将没有ORM,因为R2DBC不支持Hibernate。但是,也许。我认为这很好–作为Java开发人员,我们喜欢谈论软件设计,但是我们依赖工具,这些工具不是软件体系结构原理的最佳示例。这篇文章是关于在Spring R2DBC和PostgreSQL中没有Hibernate的实体中处理合成的。
解决方案 请考虑以下示例:我们构建了一个任务管理应用程序,并且拥有Task和Project实体。两者都存储在单独的表中,但是使用project_id键进行引用。当我们想要检索任务列表时,我们希望获得有关相应项目的信息,因此我们可以在客户端应用程序中显示它。但是,我们可以有两个单独的存储库,然后合并数据,这是非常低效的想法。更好的方法是编写一个TaskRepository返回组成对象的自定义。
步骤1.定义自定义存储库 默认情况下,它ReactiveCrudRepository是一个入口点,提供基本的CRUD功能,但仅限于此。为了提供自定义查询,我们需要使用自定义存储库对其进行扩展。为此,我们首先创建一个新接口,为自定义存储库定义合同,然后使用它扩展条目存储库:
public interface CustomTaskRepository { Flux<Task> findAllTasks (UUID userId); Flux<Task> findTasksForDay (UUID userId, LocalDate day); }
下一步骤是扩展核心TaskRepository与CustomTaskRepository,如下图所示:
TaskRepository
CustomTaskRepository
public interface TaskRepository extends ReactiveCrudRepository<Task, UUID>, CustomTaskRepository {}
步骤2.履行合同 现在,我们可以实现上述接口。请注意,Impl由于Spring DI规则,实现应以结尾结尾。在该组件中,我们需要注入一个DatabaseClient非阻塞客户端来处理数据库操作。在Spring Boot中,事情是预先配置的,因此您只需要定义一个依赖项并使用基于构造函数的DI来让Spring注入它:
public class CustomTaskRepositoryImpl implements CustomTaskRepository { private DatabaseClient client; public CustomTaskRepositoryImpl(DatabaseClient client) { this.client = client; } public Flux<Task> findAllTasks (UUID userId) { return null; } public Flux<Task> findTasksForDay (UUID userId, LocalDate day) { return null; } }
步骤3.准备SQL查询 提取组成的实体意味着我们需要使用JOIN操作。PostgreSQL有6种类型的联接操作,但这超出了本文的范围。也许将来,我会将PostgreSQL添加为我的博客的主题,但现在不是。在这里,我们使用的INNER JOIN操作将返回与两个表中的给定条件相匹配的行。
在我们的示例中,我们有Task和Project实体与相连project_id。看一下下面的代码片段:
public Flux<Task> findAllTasks (UUID userId) { String query = "SELECT task_id, task_content, is_completed, user_id, task_date, task_project, project_id, project_name " + " FROM tasks INNER JOIN projects ON task_project = project_id WHERE user_id = :userId"; return null; }
步骤4.绑定参数并执行查询 同样,要在普通JDBC中执行此操作,我们首先准备一个查询,然后执行它。在R2DBC中,我们DatabaseClient.execute()为此使用方法。我们还可以绑定一些变量,例如userId。这是使用bind()方法完成的,该方法接受两个参数:key(查询中变量的名称)和一个值。
public Flux<Task> findAllTasks (UUID userId) { String query = "SELECT task_id, task_content, is_completed, user_id, task_date, task_project, project_id, project_name " + " FROM tasks INNER JOIN projects ON task_project = project_id WHERE user_id = :userId"; Flux<Task> result = client.execute(query) .bind("userId", userId) ///... return null; }
第5步。使用Mapper处理结果 这很麻烦,因为这就是我们使用ORM框架的原因。我们需要将原始结果映射到Java对象。为此(以及促进可重用性),我们创建了一个映射器。Martin Fowler定义的这种设计模式用于在对象和数据库之间移动数据,同时使它们彼此之间以及映射器本身保持独立。这个想法显示在下面:
映射器实现
在R2DC中,该map()方法执行反应式客户端映射操作。它接受BiFunction将原始行结果映射到相应的Java模型的法线。我们可以使用如下功能接口来实现它:
public class TaskMapper implements BiFunction<Row, Object, Job> { @Override public Task apply(Row row, Object o) { UUID taskId = row.get("task_id", UUID.class); String content = row.get("task_content", String.class); Boolean completed = row.get("is_completed", Boolean.class); LocalDate createdAt = row.get("task_date", LocalDate.class); UUID projectId = row.get("project_id", UUID.class); String projectName = row.get("project_name", String.class); Project project = new Project(projectId, projectName); Task task = new Task(taskId, content, complted, createdAt, project); return task; } }
接下来,我们可以将此组件添加到我们的自定义存储库中:
public Flux<Task> findAllTasks (UUID userId) { String query = "SELECT task_id, task_content, is_completed, user_id, task_date, task_project, project_id, project_name " + " FROM tasks INNER JOIN projects ON task_project = project_id WHERE tasks.user_id = :userId"; TaskMapper mapper = new TaskMapper(); Flux<Task> result = client.execute(query) .bind("userId", userId) .map(mapper:apply) //... return null; }
步骤6.消费数据 最后一步是调用终端操作以消耗数据管道。DatabaseClient有三个可用于查询的操作:
DatabaseClient
all()=返回结果的所有行。 first() =返回整个结果的第一行。 one()=仅返回一个结果,如果结果包含更多行,则失败。 在我们的示例中,我们需要满足查询条件的所有实体,因此我们使用该all()方法,如下所示:
all()
first()
one()
public Flux<Task> findAllTasks (UUID userId) { String query = "SELECT task_id, task_content, is_completed, user_id, task_date, task_project, project_id, project_name " + " FROM tasks INNER JOIN projects ON task_project = project_id WHERE tasks.user_id = :userId"; TaskMapper mapper = new TaskMapper(); Flux<Task> result = client.execute(query) .bind("userId", userId) .map(mapper:apply) .all(); return result; }
如您所见,将R2DBC与复杂的组合对象一起使用,而不需要像Hibernate这样的ORM框架,这并不是火箭科学。R2DBC提供了一个流畅的API,并且易于使用和抽象化数据库操作,因此您可以自己实现所需的持久层逻辑。
如果您对此帖子有疑问,请随时在下面发表评论或与我联系。祝你今天过得愉快!
原文链接:http://codingdict.com