Spring Boot 中的 JPA @ManyToOne 示例
在本教程中,我将向您展示如何在 Spring Boot 中使用注释实现 Spring Data JPA 多对一示例,以实现一对多映射@ManyToOne
。你会知道:
- 如何配置 Spring Data、JPA、Hibernate 来使用数据库
- 如何使用 JPA 一对多关系定义数据模型和存储库接口
@ManyToOne
- 使用Spring JPA与数据库交互进行多对一关联的方式
- 创建Spring Rest Controller处理HTTP请求的方法
JPA @ManyToOne 是 Spring 中一对多映射的合适方法
在关系数据库中,表A和表B之间的一对多关系表示表A中的一行链接到表B中的多行,但表B中的一行仅链接到表A中的一行。
例如,您需要为一个教程博客设计数据模型,其中一个教程有很多评论。所以这是一个一对多关联。
您可以将子实体映射为父
Comment
对象 (Tutorial
) 中的集合(List of s),JPA/Hibernate 为这种情况提供了@OneToMany注释:只有父端定义了关系。我们称之为单向@OneToMany
关联。教程请访问:JPA 一对多单向示例。
类似地,当只有子端管理关系时,我们可以使用@ManyToOne注释进行单向多对一关联,其中子端 ( )通过映射外键列 ( )
Comment
拥有对其父实体 ( ) 的实体对象引用。Tutorial
tutorial_id
实现 JPA/Hibernate 一对多映射的最合适方法是与@ManyToOne 的单向多对一关联,因为:
- 使用
@OneToMany
,我们需要在父类(Tutorial)中声明一个集合(Comments),我们不能限制该集合的大小,例如,在分页的情况下。 - 使用
@ManyToOne
,您可以修改存储库:- 使用分页
- 或按多个字段排序/排序
JPA 多对一示例
我们将从头开始创建一个 Spring 项目,然后我们实现 JPA/Hibernate 多对一映射,表
tutorials
如下comments
:我们还编写 Rest API 来对 Comment 实体执行 CRUD 操作。
这些是我们需要提供的API:
方法 网址 行动 邮政 /api/tutorials/:id/comments 为教程创建新评论 得到 /api/tutorials/:id/comments 检索教程的所有评论 得到 /api/评论/:id 检索评论 :id
放 /api/评论/:id 更新评论 :id
删除 /api/评论/:id 删除评论者 :id
删除 /api/教程/:id 删除教程(及其评论) :id
删除 /api/tutorials/:id/comments 删除教程的所有评论 假设我们有这样的教程表:
以下是请求示例:
- 创建新评论:POST
/api/tutorials/[:id]/comments
之后的评论表:
- 检索特定教程的所有评论:GET
/api/tutorials/[:id]/comments
- 删除特定教程的所有评论:DELETE
/api/tutorials/[:id]/comments
查看评论表,id=2的教程评论全部被删除:
- 删除教程:DELETE
/api/tutorials/[:id]
id=3 的教程的所有评论均被CASCADE自动删除。
让我们构建 Spring Boot @ManyToOne CRUD 示例。
Spring Boot 多对一示例
技术
- 爪哇 17 / 11 / 8
- Spring Boot 3 / 2(使用 Spring Web MVC、Spring Data JPA)
- H2/PostgreSQL/MySQL
- 梅文
项目结构
让我简单解释一下。
–
Tutorial
、Comment
数据模型类对应实体和表格教程、注释。
–TutorialRepository
是CommentRepository
扩展JpaRepository的 CRUD 方法和自定义查找器方法的接口。它将自动装配在TutorialController
,中CommentController
。
–是RestControllerTutorialController
,它具有 RESTful CRUD API 请求的请求映射方法。– application.properties 中 Spring Datasource、JPA 和 Hibernate 的配置。 – pom.xml包含 Spring Boot 和 MySQL/PostgreSQL/H2 数据库的依赖项。CommentController
– 关于异常包,为了让这篇文章简单明了,我不会解释它。有关更多详细信息,您可以阅读以下教程:
@RestControllerAdvice example in Spring Boot创建和设置 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-j</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 数据源、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
为MySQL或PostgreSQLDialect
PostgreSQL spring.jpa.hibernate.ddl-auto
用于数据库初始化。我们将值设置为update
value,这样数据库中就会自动创建与定义的数据模型相对应的表。对模型的任何更改也将触发对表的更新。对于生产,该属性应该是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 # default path: h2-console spring.h2.console.path=/h2-ui
spring.datasource.url
:jdbc:h2:mem:[database-name]
适用于内存数据库和jdbc:h2:file:[path/database-name]
基于磁盘的数据库。- 我们配置
H2Dialect
H2数据库 spring.h2.console.enabled=true
告诉 Spring 启动 H2 数据库管理工具,您可以在浏览器上访问该工具:http://localhost:8080/h2-console
。spring.h2.console.path=/h2-ui
是 H2 控制台的 url,因此默认 urlhttp://localhost:8080/h2-console
将更改为http://localhost:8080/h2-ui
.
定义 JPA ManyToOne 映射的数据模型
在模型包中,我们定义
Tutorial
和Comment
类。教程有四个字段:id、title、description、published。
模型/Tutorial.java _
package com.bezkoder.spring.hibernate.onetomany.model; import jakarta.persistence.*; @Entity @Table(name = "tutorials") public class Tutorial { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tutorial_generator") 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
注解用于定义主键的生成策略。GenerationType.SEQUENCE
意味着使用数据库序列生成唯一值。
我们还指出主键的名称generator
。如果您不给它名称,则默认情况下将使用hibernate_sequence表(由持久性提供程序提供,适用于所有实体)生成 id 值。@Column
注释用于定义数据库中映射注释字段的列。
该类
Comment
具有@ManyToOne
与实体多对一关系的注释Tutorial
。optional
元素设置false
为非空关系。模型/Comment.java _
package com.bezkoder.spring.hibernate.onetomany.model; import jakarta.persistence.*; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import com.fasterxml.jackson.annotation.JsonIgnore; @Entity @Table(name = "comments") public class Comment { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_generator") private Long id; @Lob private String content; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "tutorial_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) @JsonIgnore private Tutorial tutorial; // getters and setters }
我们还使用@JoinColumn注释来指定外键列 (
tutorial_id
)。如果您不提供名称JoinColumn
,系统会自动设置该名称。@JsonIgnore
用于忽略序列化和反序列化中使用的逻辑属性。我们还使用 实现了外键的级联删除功能
@OnDelete(action = OnDeleteAction.CASCADE)
。我们设置
@ManyToOne
withFetchType.LAZY
forfetch
type:默认情况下,
@ManyToOne
关联使用FetchType.EAGER
类型fetch
,但它对性能不利:public class Comment { ... @ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "tutorial_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) private Tutorial tutorial; ... }
为多对一映射创建存储库接口
让我们创建一个存储库来与数据库交互。
在存储库包中,创建TutorialRepository
扩展.CommentRepository
JpaRepository
存储库/TutorialRepository.java
package com.bezkoder.spring.hibernate.onetomany.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import com.bezkoder.spring.hibernate.onetomany.model.Tutorial; public interface TutorialRepository extends JpaRepository<Tutorial, Long> { List<Tutorial> findByPublished(boolean published); List<Tutorial> findByTitleContaining(String title); }
存储库/CommentRepository.java
package com.bezkoder.spring.hibernate.onetomany.repository; import java.util.List; import jakarta.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; import com.bezkoder.spring.hibernate.onetomany.model.Comment; public interface CommentRepository extends JpaRepository<Comment, Long> { List<Comment> findByTutorialId(Long postId); @Transactional void deleteByTutorialId(long tutorialId); }
现在我们可以使用 JpaRepository 的方法:
save()
,findOne()
,findById()
,findAll()
,count()
,delete()
,deleteById()
... 而无需实现这些方法。我们还定义了自定义查找器方法:
findByPublished()
:返回所有published
具有输入值的教程published
。findByTitleContaining()
:返回标题包含 input 的所有教程title
。findByTutorialId()
:返回 指定的教程的所有注释tutorialId
。deleteByTutorialId()
:删除 指定的教程的所有注释tutorialId
。
该实现由Spring Data JPA自动插入。
现在我们可以看到注释的优点了
@ManyToOne
。- 对于
@OneToMany
,我们需要在父类中声明一个集合,我们不能限制该集合的大小,例如,在分页的情况下。 - 使用
@ManyToOne
,您可以修改存储库:- 要使用分页,可以在以下位置找到说明: Spring Boot 分页和过滤器示例 | Spring JPA,可分页
- 或按多个字段排序/排序: Spring Data JPA Sort/Order by multiple Columns | 春季启动
请注意,以上教程适用于
TutorialRepository
,您需要以同样的方式申请CommentRepository
。
更多派生查询位于:
Spring Boot 中的 JPA 存储库查询示例带
@Query
注释的自定义查询:
Spring JPA @Query 示例:Spring Boot 中的自定义查询您还可以在以下位置找到为此 JPA 存储库编写单元测试的方法:
Spring Boot Unit Test for JPA Repository with @DataJpaTest创建 Spring Rest API 控制器
最后,我们创建控制器,为 CRUD 操作提供 API:创建、检索、更新、删除和查找教程和评论。
控制器/TutorialController.java
package com.bezkoder.spring.hibernate.onetomany.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.CrossOrigin; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.bezkoder.spring.hibernate.onetomany.exception.ResourceNotFoundException; import com.bezkoder.spring.hibernate.onetomany.model.Tutorial; import com.bezkoder.spring.hibernate.onetomany.repository.TutorialRepository; @CrossOrigin(origins = "http://localhost:8081") @RestController @RequestMapping("/api") public class TutorialController { @Autowired TutorialRepository tutorialRepository; @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) { 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); } }
控制器/CommentController.java
package com.bezkoder.spring.hibernate.onetomany.controller; 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.CrossOrigin; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.bezkoder.spring.hibernate.onetomany.exception.ResourceNotFoundException; import com.bezkoder.spring.hibernate.onetomany.model.Comment; import com.bezkoder.spring.hibernate.onetomany.repository.CommentRepository; import com.bezkoder.spring.hibernate.onetomany.repository.TutorialRepository; @CrossOrigin(origins = "http://localhost:8081") @RestController @RequestMapping("/api") public class CommentController { @Autowired private TutorialRepository tutorialRepository; @Autowired private CommentRepository commentRepository; @GetMapping("/tutorials/{tutorialId}/comments") public ResponseEntity<List<Comment>> getAllCommentsByTutorialId(@PathVariable(value = "tutorialId") Long tutorialId) { if (!tutorialRepository.existsById(tutorialId)) { throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId); } List<Comment> comments = commentRepository.findByTutorialId(tutorialId); return new ResponseEntity<>(comments, HttpStatus.OK); } @GetMapping("/comments/{id}") public ResponseEntity<Comment> getCommentsByTutorialId(@PathVariable(value = "id") Long id) { Comment comment = commentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Not found Comment with id = " + id)); return new ResponseEntity<>(comment, HttpStatus.OK); } @PostMapping("/tutorials/{tutorialId}/comments") public ResponseEntity<Comment> createComment(@PathVariable(value = "tutorialId") Long tutorialId, @RequestBody Comment commentRequest) { Comment comment = tutorialRepository.findById(tutorialId).map(tutorial -> { commentRequest.setTutorial(tutorial); return commentRepository.save(commentRequest); }).orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId)); return new ResponseEntity<>(comment, HttpStatus.CREATED); } @PutMapping("/comments/{id}") public ResponseEntity<Comment> updateComment(@PathVariable("id") long id, @RequestBody Comment commentRequest) { Comment comment = commentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("CommentId " + id + "not found")); comment.setContent(commentRequest.getContent()); return new ResponseEntity<>(commentRepository.save(comment), HttpStatus.OK); } @DeleteMapping("/comments/{id}") public ResponseEntity<HttpStatus> deleteComment(@PathVariable("id") long id) { commentRepository.deleteById(id); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @DeleteMapping("/tutorials/{tutorialId}/comments") public ResponseEntity<List<Comment>> deleteAllCommentsOfTutorial(@PathVariable(value = "tutorialId") Long tutorialId) { if (!tutorialRepository.existsById(tutorialId)) { throw new ResourceNotFoundException("Not found Tutorial with id = " + tutorialId); } commentRepository.deleteByTutorialId(tutorialId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } }
结论
今天,我们使用 Spring Data JPA、Hibernate 与 MySQL/PostgreSQL/嵌入式数据库 (H2) 的多对一关系构建了一个 Spring Boot CRUD 示例。
我们还看到,
@ManyToOne
注释是实现 JPA 一对多映射的最合适方法,并且JpaRepository
支持进行 CRUD 操作、自定义查找方法而无需样板代码的好方法。带
@Query
注释的自定义查询:
Spring JPA @Query 示例:Spring Boot 中的自定义查询如果您想将分页添加到此 Spring 项目中,可以在以下位置找到说明:
Spring Boot 分页和过滤器示例 | Spring JPA,可分页按多个字段排序/排序:
Spring Data JPA 按多个列排序/排序| 春季启动处理此 Rest API 的异常是必要的:
或者为 JPA 存储库编写单元测试的方法:
Spring Boot Unit Test for JPA Repository with @DataJpaTest- 使用
- 点赞
- 收藏
- 关注作者
评论(0)