JPA/Hibernate One To One在Spring Boot中的单向映射

在本教程中,我将向您展示如何在使用@OneToOne注解的Spring Boot示例中实现Spring JPA的一对一单向映射与Hibernate。您将了解:

  • 如何配置Spring Data、JPA和Hibernate以与数据库一起工作

  • 如何为JPA一对一关系定义数据模型和存储库接口

  • 使用Spring JPA与数据库进行一对一关联交互的方法

  • 创建处理HTTP请求的Spring Rest Controller的方法

目录

  1. 适当的实现方式

  2. JPA一对一示例

  3. 通过Spring Boot示例进行实践

  4. 技术框架

  5. 项目结构

  6. 设置Spring Boot项目

  7. 配置Spring数据源、JPA和Hibernate

  8. 定义数据模型

  9. 创建存储库接口

  10. 创建Spring Rest API控制器

  11. 总结

  12. 源代码

  13. 进一步阅读

实现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)

缓存:Spring Boot Redis缓存示例文章来源地址https://www.toymoban.com/diary/java/697.html

到此这篇关于JPA/Hibernate One To One在Spring Boot中的单向映射的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

原文地址:https://www.toymoban.com/diary/java/697.html

如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用
使用 js 根据文章中h标签生成目录结构,生成树状结构
上一篇 2024年01月20日 15:57
Spring Boot中的自定义查询示例:使用Spring JPA @Query
下一篇 2024年01月21日 01:23

相关文章

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包