Spring Boot 中的 JPA @ManyToOne 示例

举报
千锋教育 发表于 2023/08/14 16:55:47 2023/08/14
【摘要】 在本教程中,我将向您展示如何在 Spring Boot 中使用注释实现 Spring Data JPA 多对一示例,以实现一对多映射@ManyToOne。你会知道:如何配置 Spring Data、JPA、Hibernate 来使用数据库如何使用 JPA 一对多关系定义数据模型和存储库接口@ManyToOne使用Spring JPA与数据库交互进行多对一关联的方式创建Spring Rest ...

在本教程中,我将向您展示如何在 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拥有对其父实体 ( ) 的实体对象引用。Tutorialtutorial_id

    实现 JPA/Hibernate 一对多映射的最合适方法是与@ManyToOne 的单向多对一关联,因为:

    • 使用@OneToMany,我们需要在父类(Tutorial)中声明一个集合(Comments),我们不能限制该集合的大小,例如,在分页的情况下。
    • 使用@ManyToOne,您可以修改存储库:
      • 使用分页
      • 或按多个字段排序/排序

    JPA 多对一示例

    我们将从头开始创建一个 Spring 项目,然后我们实现 JPA/Hibernate 多对一映射,表tutorials如下comments

    jpa 多对一图

    我们还编写 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 删除教程的所有评论

    假设我们有这样的教程表:

    jpa-manytoone-父表

    以下是请求示例:

    • 创建新评论:POST/api/tutorials/[:id]/comments

    jpa-manytoone-示例-创建子实体

    之后的评论表:

    jpa-manytoone-子表

    • 检索特定教程的所有评论:GET/api/tutorials/[:id]/comments

    jpa-manytoone-示例-spring-crud-retrieve

    • 删除特定教程的所有评论:DELETE/api/tutorials/[:id]/comments

    jpa-manytoone-示例-spring-crud-删除

    查看评论表,id=2的教程评论全部被删除:

    jpa-manytoone-示例-spring-crud-表-删除

    • 删除教程:DELETE/api/tutorials/[:id]

    jpa-manytoone-示例-spring-crud-删除级联

    jpa-manytoone-示例-spring-crud-删除-父表

    id=3 的教程的所有评论均被CASCADE自动删除。

    jpa-manytoone-示例-spring-crud-删除级联表

    让我们构建 Spring Boot @ManyToOne CRUD 示例。

    Spring Boot 多对一示例

    技术

    • 爪哇 17 / 11 / 8
    • Spring Boot 3 / 2(使用 Spring Web MVC、Spring Data JPA)
    • H2/PostgreSQL/MySQL
    • 梅文

    项目结构

    jpa-manytoone-示例-hibernate-spring-boot-项目结构

    让我简单解释一下。

    – TutorialComment数据模型类对应实体和表格教程注释
    TutorialRepositoryCommentRepository扩展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或PostgreSQLDialectPostgreSQL
    • spring.jpa.hibernate.ddl-auto用于数据库初始化。我们将值设置为updatevalue,这样数据库中就会自动创建与定义的数据模型相对应的表。对模型的任何更改也将触发对表的更新。对于生产,该属性应该是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.urljdbc:h2:mem:[database-name]适用于内存数据库和jdbc:h2:file:[path/database-name]基于磁盘的数据库。
    • 我们配置H2DialectH2数据库
    • 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 映射的数据模型

    模型包中,我们定义TutorialComment类。

    教程有四个字段: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与实体多对一关系的注释Tutorialoptional元素设置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)

    我们设置@ManyToOnewith FetchType.LAZYfor fetchtype:

    jpa-manytoone-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;
    
      ...
    }
    

    jpa-manytoone-eager-fetch

    为多对一映射创建存储库接口

    让我们创建一个存储库来与数据库交互。
    存储库包中,创建TutorialRepository扩展.CommentRepositoryJpaRepository

    存储/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

    更多派生查询位于:
    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

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。