在本教程中,我将向您展示如何在Spring Boot中实现Spring Data JPA多一对一的示例,以使用@ManyToOne
注释进行一对多映射。您会知道:
- 如何配置弹簧数据,JPA,Hibernate与数据库一起工作
- 如何使用
@ManyToOne
定义JPA一对多关系的数据模型和存储库接口 - 使用Spring JPA与多对一关联的数据库进行交互的方式
- 创建Spring Rest Controller来处理HTTP请求 的方法
相关文章:
- JPA One To One example with Hibernate in Spring Boot
- JPA Many to Many example with Hibernate in Spring Boot
- Validate Request Body in Spring Boot
- Spring Boot Token based Authentication with Spring Security & JWT
- Spring JPA + H2 example
- Spring JPA + MySQL example
- Spring JPA + PostgreSQL example
- Spring JPA + Oracle example
- Spring JPA + SQL Server example
- 文档:Spring Boot Swagger 3 example
- Caching: Spring Boot Redis Cache example
JPA @manytoone是适合春季映射的一项映射
在关系数据库中,表A和表B之间的一对多关系表明表A中的一个行链接到表B中的许多行,但表B中的一行仅链接到表A中的一行。
例如,您需要为一个教程博客设计数据模型,其中一个教程具有许多评论。所以这是一个一对多的协会。
您可以将儿童实体映射为父对象(Tutorial
)中的集合(Comment
s列表),而jpa/hibernate为这种情况提供了@OneToMany注释:只有父端定义了关系。我们称其为单向@OneToMany
协会。
有关教程,请访问:JPA One To Many Unidirectional example。
同样,当只有儿童侧管理关系时,我们与@ManyToOne注释有多一的单向关联,其中儿童(Comment
)通过映射外国钥匙具有实体对象对其母体实体(Tutorial
)的参考。列(tutorial_id
)。
实施JPA/Hibernate对许多映射的最合适方法是与@ManyToOne的单向相关性,因为:
- 使用
@OneToMany
,我们需要在父级(教程)中声明一个集合(注释),我们不能限制该集合的大小,例如,在分页时。 - 使用
@ManyToOne
,您可以修改存储库:- 与分页合作
- 或通过多个字段进行排序/订单
jpa多人示例
我们将从头开始创建一个春季项目,然后我们用tutorials
和comments
表实现JPA/Hibernate,如下:
我们还编写REST API,以对评论实体进行CRUD操作。
这些是我们需要提供的API:
方法 | urls | 动作 |
---|---|---|
post | /api/tutorials/:id/评论 | 为教程创建新评论 |
get | /api/tutorials/:id/评论 | 检索教程的所有评论 |
get | /api/comment/:id | 检索:id 的评论
|
put | /api/comment/:id | 更新:id 的评论
|
delete | /api/comment/:id | 删除:id 的评论
|
delete | /api/tutorials/:ID | 删除:id 的教程(及其评论)
|
delete | /api/tutorials/:id/评论 | 删除教程的所有评论 |
假设我们有教程表:
这是示例请求:
- 创建新评论:发布
/api/tutorials/[:id]/comments
注释之后:
- 检索特定教程的所有评论:获取
/api/tutorials/[:id]/comments
- 删除特定教程的所有评论:删除
/api/tutorials/[:id]/comments
检查注释表,删除了带有ID = 2的教程的所有评论:
- 删除教程:删除
/api/tutorials/[:id]
ID = 3的教程的所有评论都是 CASCADE 自动删除。
让我们构建我们的春季靴子@manytoone crud示例。
春季靴子多个示例
技术
- Java 17/11/8
- Spring Boot 3/2(带有Spring Web MVC,Spring Data JPA)< / li>
- h2/postgresql/mysql
- Maven
项目结构
让我简要解释。
Tutorial
,Comment
数据模型类对应于实体和表教程,注释。
TutorialRepository
,CommentRepository
是延伸JpaRepository的接口,用于CRUD方法和自定义查找器方法。它将在TutorialController
,CommentController
中自动进行。
TutorialController
,CommentController
是RestControllers,它请求对静止的CRUD API请求请求映射方法。
< application.properties 。
pom.xml 包含Spring Boot和MySQL/PostgreSQL/H2数据库的依赖性。
关于例外包装,要使此帖子保持直接,我不会解释。有关更多详细信息,您可以阅读以下教程:
@RestControllerAdvice example in Spring Boot
创建&设置Spring Boot Project
使用Spring web tool或您的开发工具(Spring Tool Suite,Eclipse,Intellij)创建一个春季启动项目。
然后打开 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 DataSource,JPA,Hibernate
在src/main/resources文件夹下,打开应用程序。
- 对于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实现,我们为MySQL或
PostgreSQLDialect
配置MySQLDialect
for Postgresql -
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
# default path: 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,因此默认URLhttp://localhost:8080/h2-console
将更改为http://localhost:8080/h2-ui
。
定义了JPA的数据模型,
在模型软件包中,我们定义Tutorial
和Comment
类。
教程有四个字段:ID,标题,描述,已发布。
模型/ 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
的名称。如果您不给它名称,则ID值将使用 hibernate_squerence 表(默认情况下由Persistence提供者提供)。 -
@Column
注释用于定义映射注释字段的数据库中的列。
Comment
类具有与Tutorial
实体的多对一关系的@ManyToOne
注释。 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
设置为FetchType.LAZY
用于fetch
类型:
默认情况下,@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);
}
现在我们可以使用JPAREPOSITOR的方法:save()
,findOne()
,findById()
,findAll()
,count()
,count()
,delete()
,deleteById()
...
我们还定义自定义查找器方法:
-
findByPublished()
:返回所有教程,带有published
的价值为输入published
。 -
findByTitleContaining()
:返回所有标题包含输入title
的教程。 -
findByTutorialId()
:返回tutorialId
指定的教程的所有评论。 -
deleteByTutorialId()
:删除tutorialId
指定的教程的所有评论。
该实现由Spring Data JPA自动插入。
现在我们可以看到@ManyToOne
注释的专利。
- 使用
@OneToMany
,我们需要在父级中声明一个集合,我们不能限制该集合的大小,例如,如果是分页。 - 使用
@ManyToOne
,您可以修改存储库:- 要处理分页,可以找到该说明: Spring Boot Pagination & Filter example | Spring JPA, Pageable
- 或通过多个字段进行排序/订单: Spring Data JPA Sort/Order by multiple Columns | Spring Boot
请注意,上面的教程适用于
TutorialRepository
,您需要以相同的方式为CommentRepository
。
更多的派生查询:
JPA Repository query example in Spring Boot
用@Query
注释自定义查询:
Spring JPA @Query example: Custom query in Spring Boot
您还找到了为此JPA存储库编写单元测试的方法:
Spring Boot Unit Test for JPA Repository with @DataJpaTest
最后,我们创建为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);
}
}
结论
今天,我们已经使用弹簧数据JPA建立了一个春季靴子CRUD示例,与MySQL/PostgreSQL/Embedded Database(H2)冬眠了许多关系。
我们还看到@ManyToOne
注释是实施JPA一对下映射的最合适的方法,而JpaRepository
支持一种制作CRUD操作的好方法,无需样板板代码。
用@Query
注释自定义查询:
Spring JPA @Query example: Custom query in Spring Boot
如果要在这个春季项目中添加分页,可以在:
上找到指令
Spring Boot Pagination & Filter example | Spring JPA, Pageable
通过多个字段进行排序/顺序:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot
处理此REST API的例外是必要的:
- Spring Boot @ControllerAdvice & @ExceptionHandler example
- @RestControllerAdvice example in Spring Boot
或为JPA存储库编写单元测试的方法:
Spring Boot Unit Test for JPA Repository with @DataJpaTest
您也可以知道:
- Validate Request Body in Spring Boot
- 如何使用this tutorial在AWS上部署这个春季启动应用。
- 与Docker Compose: Spring Boot and MySQL example dockerize
- 使用this post上传Excel文件并将数据存储在MySQL数据库中
- 上传CSV文件,并使用this post将数据存储在MySQL中。
快乐学习!再次见。
进一步阅读
- Secure Spring Boot App with Spring Security & JWT Authentication
- Spring Data JPA Reference Documentation
- Spring Boot Pagination and Sorting example
Fullstack Crud应用:
- Vue + Spring Boot example
- Angular 8 + Spring Boot example
- Angular 10 + Spring Boot example
- Angular 11 + Spring Boot example
- Angular 12 + Spring Boot example
- Angular 13 + Spring Boot example
- Angular 14 + Spring Boot example
- Angular 15 + Spring Boot example
- Angular 16 + Spring Boot example
- React + Spring Boot example
源代码
您可以在Github上找到本教程的完整源代码。
而不是使用@OneToMany
:JPA One To Many Unidirectional example
多对多:JPA Many to Many example with Hibernate in Spring Boot
一对一:JPA One To One example with Hibernate in Spring Boot
您可以在以下教程中应用此实现:
- Spring JPA + H2 example
- Spring JPA + MySQL example
- Spring JPA + PostgreSQL example
- Spring JPA + Oracle example
- Spring JPA + SQL Server example
更多的派生查询:
JPA Repository query example in Spring Boot
文档:Spring Boot + Swagger 3 example (with OpenAPI 3)
缓存:Spring Boot Redis Cache example