在本教程中,我将向您展示如何在使用@OneToOne注解的Spring Boot示例中实现Spring JPA的一对一单向映射与Hibernate。您将了解:
如何配置Spring Data、JPA和Hibernate以与数据库一起工作
如何为JPA一对一关系定义数据模型和存储库接口
使用Spring JPA与数据库进行一对一关联交互的方法
创建处理HTTP请求的Spring Rest Controller的方法
目录
适当的实现方式
JPA一对一示例
通过Spring Boot示例进行实践
技术框架
项目结构
设置Spring Boot项目
配置Spring数据源、JPA和Hibernate
定义数据模型
创建存储库接口
创建Spring Rest API控制器
总结
源代码
进一步阅读
实现JPA/Hibernate一对一映射的适当方式
在关系数据库中,表A和表B之间的一对一关系表示表A中的一行仅链接到表B中的一行,反之亦然。
例如,您需要为教程博客设计数据模型,其中一个教程具有相应的详细信息(创建日期时间、创建者)。因此,这是一个一对一关联。
您可以使用Join Table(带有@JoinTable注解)来实现。它将两个实体的主键值存储在一个新表中。
另一种方法是使用共享主键,其中外键位于tutorial_details表中。Tutorial实体是父实体,而Tutorial Details是子实体。
您可以使用JPA/Hibernate的@OneToOne注解将子实体与父实体进行映射。在这种情况下,只有子侧定义了关系。我们称之为单向一对一关联。
现在看看tutorial_details表,它包含一个主键列(id)和一个外键列(tutorial_id)。您可以看到,我们真正只需要一个与教程(父实体)关联的tutorial_details(子实体)行,并且子数据在其他关系中几乎不会被使用。因此,我们可以省略子实体的id列:
JPA一对一示例
我们将从头开始创建一个Spring项目,然后按照以下步骤实现JPA/Hibernate的一对一单向映射,使用tutorials和tutorial_details表:
我们还编写了REST API来执行对Details实体的CRUD操作。
以下是需要提供的API:
方法 | URL | 操作 |
---|---|---|
POST | /api/tutorials/:id/details | 为 Tutorial 创建新的 Details |
GET | /api/details/:id | 通过 :id 检索 Details |
GET | /api/tutorials/:id/details | 检索 Tutorial 的 Details |
PUT | /api/details/:id | 通过 :id 更新 Details |
PUT | /api/tutorials/:id/details | 更新 Tutorial 的 Details |
DELETE | /api/details/:id | 通过 :id 删除 Details |
DELETE | /api/tutorials/:id/details | 删除 Tutorial 的 Details |
DELETE | /api/tutorials/:id | 删除 Tutorial(及其 Details) |
假设我们已经有了如下的tutorials表格:
以下是示例请求:
创建新的Details实体:POST /api/tutorials/[:id]/details
此后的tutorial_details表格如下所示:
检索特定Tutorial的Details:GET /api/tutorials/[:id]/details 或 /api/details/[:id]
更新特定Tutorial的Details:PUT /api/tutorials/[:id]/details 或 /api/details/[:id]
删除特定Tutorial的Details:DELETE /api/tutorials/[:id]/details 或 /api/details/[:id]
请检查tutorial_details表格,教程ID为1的行已被删除:
删除教程:DELETE /api/tutorials/[:id]
教程(id=3)及其Details已被删除:
让我们构建我们的Spring Boot Data JPA一对一示例。
Spring Boot 一对一示例
技术栈
Java 17 / 11 / 8
Spring Boot 3 / 2(包括Spring Web MVC,Spring Data JPA)
H2/MySQL/PostgreSQL
Maven
项目结构
让我简要解释一下。
Tutorial和TutorialDetails数据模型类对应于实体和表tutorials、tutorial_details。
TutorialRepository、TutorialDetailsRepository是扩展JpaRepository的接口,用于CRUD方法和自定义查找方法。它们将在TutorialController、TutorialDetailsController中进行自动注入。
TutorialController、TutorialDetailsController是RestControllers,具有用于RESTful CRUD API请求的RequestMapping方法。
application.properties中配置了Spring Datasource、JPA和Hibernate。
pom.xml包含了Spring Boot和MySQL/PostgreSQL/H2数据库的依赖项。
关于异常包,为了保持本文简洁,我不会进行解释。如果需要更多细节,您可以阅读以下教程: 在Spring Boot中使用@RestControllerAdvice的示例
创建和设置Spring Boot项目
使用Spring Web工具或您的开发工具(Spring Tool Suite,Eclipse,Intellij)创建一个Spring Boot项目。
然后打开pom.xml文件,并添加以下依赖项:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
我们还需要添加一个额外的依赖项。
如果您想使用MySQL:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
或者PostgreSQL:
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency>
或者H2(嵌入式数据库):
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
配置Spring Datasource,JPA和Hibernate
在src/main/resources文件夹下,打开application.properties文件并编写以下内容。
对于MySQL:
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false spring.datasource.username=root spring.datasource.password=123456 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect # Hibernate ddl auto (create, create-drop, validate, update) spring.jpa.hibernate.ddl-auto=update
对于PostgreSQL:
spring.datasource.url=jdbc:postgresql://localhost:5432/testdb spring.datasource.username=postgres spring.datasource.password=123 spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # Hibernate ddl auto (create, create-drop, validate, update) spring.jpa.hibernate.ddl-auto=update
其中spring.datasource.username和spring.datasource.password属性与您的数据库安装相同。
Spring Boot使用Hibernate作为JPA实现,我们根据数据库类型配置MySQLDialect或PostgreSQLDialect。
spring.jpa.hibernate.ddl-auto用于数据库初始化。我们将其值设置为update,这样表将自动在数据库中创建,对应于定义的数据模型。对模型的任何更改也将触发对表的更新。在生产环境中,此属性应该是validate。
对于H2数据库:
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update spring.h2.console.enabled=true # 默认路径:h2-console spring.h2.console.path=/h2-ui
spring.datasource.url: jdbc:h2:mem:[database-name]用于内存数据库,jdbc:h2:file:[path/database-name]用于基于磁盘的数据库。
我们为H2数据库配置了H2Dialect。
spring.h2.console.enabled=true告诉Spring启动H2数据库管理工具,并且您可以通过浏览器访问此工具:http://localhost:8080/h2-console。
spring.h2.console.path=/h2-ui是为H2控制台的URL设置,因此默认URL http://localhost:8080/h2-console 将改为 http://localhost:8080/h2-ui。
为JPA一对一映射定义数据模型
在model包中,我们定义Tutorial和TutorialDetails类。
Tutorial有四个字段:id、title、description、published。
model/Tutorial.java
package com.bezkoder.spring.jpa.onetoone.model; import jakarta.persistence.*; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties({"hibernateLazyInitializer"}) @Entity @Table(name = "tutorials") public class Tutorial { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "title") private String title; @Column(name = "description") private String description; @Column(name = "published") private boolean published; public Tutorial() { } public Tutorial(String title, String description, boolean published) { this.title = title; this.description = description; this.published = published; } // getters and setters }
@Entity注解表示该类是一个持久化Java类。
@Table注解指定了映射到此实体的表名。
@Id注解用于主键。
@GeneratedValue注解用于定义主键的生成策略。
@Column注解用于定义数据库中映射的列。
那么@JsonIgnoreProperties({"hibernateLazyInitializer"})呢?
当我们使用JPA Repository从数据库中获取数据时,对于从父实体进行懒加载的字段,Hibernate返回一个具有映射到表的所有字段以及hibernateLazyInitializer的对象。
然后,当我们将此实体序列化为JSON字符串格式时,所有字段和hibernateLazyInitializer都将被序列化。
因此,为了避免这种不必要的序列化,我们使用@JsonIgnoreProperties。
TutorialDetails类使用@OneToOne注解与Tutorial实体建立一对一关系,并使用@MapsId注解将id字段同时作为主键和外键(共享主键)。
我们通过使用@JoinColumn注解来设置共享主键列名。
model/TutorialDetails.java
package com.bezkoder.spring.jpa.onetoone.model; import java.util.Date; import jakarta.persistence.*; @Entity @Table(name = "tutorial_details") public class TutorialDetails { @Id private Long id; @Column private Date createdOn; @Column private String createdBy; @OneToOne(fetch = FetchType.LAZY) @MapsId @JoinColumn(name = "tutorial_id") private Tutorial tutorial; public TutorialDetails() { } public TutorialDetails(String createdBy) { this.createdOn = new Date(); this.createdBy = createdBy; } // getters and setters }
让我们创建一个与数据库交互的仓库。
在repository包中,创建TutorialRepository和TutorialDetailsRepository接口,它们都扩展了JpaRepository。
repository/TutorialRepository.java
package com.bezkoder.spring.jpa.onetoone.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.bezkoder.spring.jpa.onetoone.model.Tutorial; @Repository public interface TutorialRepository extends JpaRepository<Tutorial, Long> { List<Tutorial> findByPublished(boolean published); List<Tutorial> findByTitleContaining(String title); } ``` repository/TutorialDetailsRepository.java ```java package com.bezkoder.spring.jpa.onetoone.repository; import jakarta.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.bezkoder.spring.jpa.onetoone.model.TutorialDetails; @Repository public interface TutorialDetailsRepository extends JpaRepository<TutorialDetails, Long> { @Transactional void deleteById(long id); @Transactional void deleteByTutorialId(long tutorialId); }
现在我们可以使用JpaRepository的方法:save()、findOne()、findById()、findAll()、count()、delete()、deleteById()等,而无需实现这些方法。
我们还定义了自定义的查找方法:
findByPublished():返回所有published字段值为输入published的Tutorial。
findByTitleContaining():返回所有标题包含输入title的Tutorial。
deleteById():根据id删除指定的Tutorial Details。
deleteByTutorialId():根据tutorialId删除指定Tutorial的Details。
Spring Data JPA会自动插入实现。
使用@Query注解进行自定义查询:
Spring Boot中的自定义查询示例:使用Spring JPA @Query
您还可以找到有关为此JPA Repository编写单元测试的方法:
@DataJpaTest的Spring Data Repository单元测试示例
Create Spring Rest APIs Controller
最后,我们创建一个Controller提供CRUD操作的API:创建、获取、更新、删除和查找Tutorial和Details。
controller/TutorialController.java
package com.bezkoder.spring.jpa.onetoone.controller; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.bezkoder.spring.jpa.onetoone.exception.ResourceNotFoundException; import com.bezkoder.spring.jpa.onetoone.model.Tutorial; import com.bezkoder.spring.jpa.onetoone.repository.TutorialDetailsRepository; import com.bezkoder.spring.jpa.onetoone.repository.TutorialRepository; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api") public class TutorialController { @Autowired TutorialRepository tutorialRepository; @Autowired private TutorialDetailsRepository detailsRepository; @GetMapping("/tutorials") public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) { List<Tutorial> tutorials = new ArrayList<Tutorial>(); if (title == null) tutorialRepository.findAll().forEach(tutorials::add); else tutorialRepository.findByTitleContaining(title).forEach(tutorials::add); if (tutorials.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } return new ResponseEntity<>(tutorials, HttpStatus.OK); } @GetMapping("/tutorials/{id}") public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) { Tutorial tutorial = tutorialRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id)); return new ResponseEntity<>(tutorial, HttpStatus.OK); } @PostMapping("/tutorials") public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) { Tutorial _tutorial = tutorialRepository.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), true)); return new ResponseEntity<>(_tutorial, HttpStatus.CREATED); } @PutMapping("/tutorials/{id}") public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) { Tutorial _tutorial = tutorialRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id)); _tutorial.setTitle(tutorial.getTitle()); _tutorial.setDescription(tutorial.getDescription()); _tutorial.setPublished(tutorial.isPublished()); return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK); } @DeleteMapping("/tutorials/{id}") public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) { if (detailsRepository.existsById(id)) { detailsRepository.deleteById(id); } tutorialRepository.deleteById(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/tutorials") public ResponseEntity<HttpStatus> deleteAllTutorials() { tutorialRepository.deleteAll(); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @GetMapping("/tutorials/published") public ResponseEntity<List<Tutorial>> findByPublished() { List<Tutorial> tutorials = tutorialRepository.findByPublished(true); if (tutorials.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } return new ResponseEntity<>(tutorials, HttpStatus.OK); } }
controller/TutorialDetailsController.java
package com.bezkoder.spring.jpa.onetoone.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.bezkoder.spring.jpa.onetoone.exception.ResourceNotFoundException; import com.bezkoder.spring.jpa.onetoone.model.TutorialDetails; import com.bezkoder.spring.jpa.onetoone.model.Tutorial; import com.bezkoder.spring.jpa.onetoone.repository.TutorialDetailsRepository; import com.bezkoder.spring.jpa.onetoone.repository.TutorialRepository; @CrossOrigin(origins = "*") @RestController @RequestMapping("/api") public class TutorialDetailsController { @Autowired private TutorialDetailsRepository detailsRepository; @Autowired private TutorialRepository tutorialRepository; @GetMapping({ "/details/{id}", "/tutorials/{id}/details" }) public ResponseEntity<TutorialDetails> getDetailsById(@PathVariable(value = "id") Long id) { TutorialDetails details = detailsRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial Details with id = " + id)); return new ResponseEntity<>(details, HttpStatus.OK); } @PostMapping("/tutorials/{tutorialId}/details") public ResponseEntity<TutorialDetails> createDetails(@PathVariable(value = "tutorialId") Long tutorialId, @RequestBody TutorialDetails detailsRequest) { Tutorial tutorial = tutorialRepository.findById(tutorialId) .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId)); detailsRequest.setCreatedOn(new java.util.Date()); detailsRequest.setTutorial(tutorial); TutorialDetails details = detailsRepository.save(detailsRequest); return new ResponseEntity<>(details, HttpStatus.CREATED); } @PutMapping("/details/{id}") public ResponseEntity<TutorialDetails> updateDetails(@PathVariable("id") long id, @RequestBody TutorialDetails detailsRequest) { TutorialDetails details = detailsRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Id " + id + " not found")); details.setCreatedBy(detailsRequest.getCreatedBy()); return new ResponseEntity<>(detailsRepository.save(details), HttpStatus.OK); } @DeleteMapping("/details/{id}") public ResponseEntity<HttpStatus> deleteDetails(@PathVariable("id") long id) { detailsRepository.deleteById(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/tutorials/{tutorialId}/details") public ResponseEntity<TutorialDetails> deleteDetailsOfTutorial(@PathVariable(value = "tutorialId") Long tutorialId) { if (!tutorialRepository.existsById(tutorialId)) { throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId); } detailsRepository.deleteByTutorialId(tutorialId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } }
这些控制器使用@Autowired注释注入了TutorialRepository和TutorialDetailsRepository。它们定义了与数据库交互的各种API,包括获取、创建、更新和删除Tutorial和Details。根据需求,您可以自由调整这些控制器的代码。
总结
今天我们使用Spring Data JPA和Hibernate构建了一个使用MySQL/PostgreSQL/嵌入式数据库(H2)的Spring Boot示例。
我们还看到,@OneToOne和@MapsId注解是实现JPA一对一单向映射的适当方式,而JpaRepository则支持在不需要样板代码的情况下进行CRUD操作和自定义查找方法。
使用@Query注解进行自定义查询:
Spring JPA @Query示例:在Spring Boot中进行自定义查询
如果您想为这个Spring项目添加分页功能,可以在以下链接找到相关指南:
Spring Boot分页&过滤示例 | Spring JPA,Pageable
对多个字段进行排序:
Spring Data JPA按多列排序/排序 | Spring Boot
处理Rest API的异常非常重要:
Spring Boot @ControllerAdvice和@ExceptionHandler示例
在Spring Boot中使用@RestControllerAdvice示例
或者,可以学习如何编写JPA Repository的单元测试:
使用@DataJpaTest进行Spring Boot JPA Repository单元测试
您还可以了解以下内容:
在Spring Boot中验证请求体
如何使用本教程将Spring Boot应用部署到AWS(免费)
使用Docker Compose实现Spring Boot和MySQL的容器化示例
或者:Docker Compose实现Spring Boot和Postgres的容器化示例
如何上传Excel文件并将数据存储在MySQL数据库中
上传CSV文件并将数据存储在MySQL中
祝您学习愉快!再见。
进一步阅读
使用Spring Security和JWT认证保护Spring Boot应用程序
Spring Data JPA参考文档
Spring Boot分页和排序示例
完整的CRUD应用程序:
Vue + Spring Boot示例
Angular 8 + Spring Boot示例
Angular 10 + Spring Boot示例
Angular 11 + Spring Boot示例
Angular 12 + Spring Boot示例
Angular 13 + Spring Boot示例
Angular 14 + Spring Boot示例
Angular 15 + Spring Boot示例
Angular 16 + Spring Boot示例
React + Spring Boot示例
源代码
您可以在Github上找到本教程的完整源代码。
一对多关系:使用Hibernate和Spring Boot的JPA一对多示例
多对多关系:在Spring Boot中使用Hibernate的JPA多对多示例
您可以将此实现应用于以下教程:
Spring JPA + H2示例
Spring JPA + MySQL示例
Spring JPA + PostgreSQL示例
Spring JPA + Oracle示例
Spring JPA + SQL Server示例
更多派生查询请参阅:
Spring Boot中的JPA Repository查询示例
文档:Spring Boot + Swagger 3示例(使用OpenAPI 3)文章来源:https://www.toymoban.com/article/697.html
缓存:Spring Boot Redis缓存示例文章来源地址https://www.toymoban.com/article/697.html
到此这篇关于JPA/Hibernate One To One在Spring Boot中的单向映射的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!