当前位置: 首页 > article >正文

[spring] Spring JPA - Hibernate 多表联查 1

[spring] Spring JPA - Hibernate 多表联查 1

之前在 [spring] spring jpa - hibernate 名词解释&配置 和 [spring] spring jpa - hibernate CRUD 简单的学习了一下怎么使用 Hibernate 实现 CRUD 操作,不过涉及到的部分都是逻辑上比较简单的实现——只在一张表上进行操作

关于多表关联的部分,只是在关键字中提到了有提到过 Relationship 的注解,之后这三章内容就会对 Relationship 进行学习

基础概念

这里回顾一下数据库的基础概念,具体的使用方式在后面的笔记中会被提到,所以这里也就不 cv 代码了

  • 主键(Primary Key)& 外键(Foreign Key)

    主键是一个表中的一个或多个列,能够表示当前数据在当前表中的唯一性,每个表只能有一个主键

    比较常见用一个列作为主键的情况有学生 id,课程 id,书籍 id,图书馆 id 等

    用多个列作为主键的情况,一般代表单独的一个列无法完整表达对应信息,如学生选修的课程,这种情况下主键可以使用学生 id+课程 id+年份,或者图书的出借记录,可以使用学生 id+书籍 id+出借日期等

    外键用于引用另一张表的主键,如上面多个列作为主键的情况中,学生 id、书籍 id 都是外键

  • 级联(Cascade)

    Cascade 是一种为了维护数据库完整性的机制,这里不会过多的设计到数据库的 Cascade,而是提一下 hibernate 中的 Cascade 类型,也就是 CascadeType

    • PERSIST 代表当持久化一个实体时,其关联的实体也会被实体化

    • MERGE 代表会将 detached entity 的变更数据 复制到当前 管理中的 entity。如果数据库中已有该 entity,则会 更新 现有记录;如果没有,则会 插入 新记录。

      虽然说是 merge 操作,不过大多数情况下 merge 操作被用来当做更新

    • REMOVE 代表当删除一个实体时,其关联的实体也会被删除 (如果启用 CascadeType.REMOVE

    • REFRESH 表从 数据库重新加载最新数据,并且 丢弃内存中未提交的更改

    • DETACH 代表当更新一个分离时,其关联的实体也会被分离

      这个主要代表当前的 entity 不再被持久层所管理

    • ALL 代表上述所有的操作都会被执行

    默认情况下,hibernate 不会 cascade 任何操作

  • 数据加载——eager & lazy

    这代表的是两种数据的获取方式,eager 代表当 entity 被获取时,其关联的所有实体也会被同时获取

  • 关系

    • one to one

      即一对一的关系,比如说 国家首都用户用户信息 这种都是比较常见的一对一的关系

    • one to many

      一对多,也是多对一的关系,比较常见的有 作者书籍学生学校

    • many to many

      多对多的关系,这应该是最常见的情况了,比如说 作者出版商学生

    需要注意的是,不同的关系在不同的系统中都会有些微的差异,并非不可改变。比如说以电话系统为例,虽然日常生活中,常见的案例为 一号一人,设计上可能会偏好将 电话号码 以 一对多 的关系进行构建。但是在 ToB 的业务中就需要考虑到公司号码其实会被分拨到不同的客服手上的情况,相对于 ToC 端 one-to-many 的设计,toB 端可能就要进行 many-to-many 的设计修正

  • 实体生命周期

    hibernate session 中的生命周期有 4 个:

    • transient

      new instance,如果没有引用就会被 GC 回收

    • persist

      与 persistence context 关联,这个情况下,hibernate 会对数据的更新进行管理和同步

    • detached

      hibernate 不再对数据进行管理

    • removed

      准备彻底被删除

    geeksforgeeks 上找到了个图描述了一下生命周期:

    hibernate session lifecycle

    整体来说这个部分还是比较复杂的,简单的几句话很难描述整个生命周期的流程,图里也缺少了一些必要的 op,比如 rollback 之类的,有空再补充一下 hibernate 好了

这章笔记主要过一遍 1-1 的数据关联,以及原始笔记是很久之前写的了,我重新走了一下流程补充了点内容,所以项目名称会有点差异……

one-to-one uni-relational

instructor details
instructor

uni-relational 的关系如上面图所示,即 A 能够找到 B 的关联,但是 B 无法回溯到 A

设置数据库

脚本如下:

DROP SCHEMA IF EXISTS `hb-01-one-to-one-uni`;

CREATE SCHEMA `hb-01-one-to-one-uni`;

use `hb-01-one-to-one-uni`;

SET FOREIGN_KEY_CHECKS = 0;

CREATE TABLE `instructor_detail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `youtube_channel` varchar(128) DEFAULT NULL,
  `hobby` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;


CREATE TABLE `instructor` (
  `id` int NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  `instructor_detail_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_DETAIL_idx` (`instructor_detail_id`),
  CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`) REFERENCES `instructor_detail` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;

ER 图如下:

在这里插入图片描述

ER 图是 dbeaver(有免费版)根据对应的数据库自动生成的,有图就可以证明 ER 图生成的没问题了

配置项目

新建项目

依旧用 spring initializer 实现:

在这里插入图片描述

main 文件如下:

package com.example.demo;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Bean
	public CommandLineRunner commandLineRunner(String[] args) {
		return runner -> {
			System.out.println("Hello World!");
		};
	}
}

这样就创建了一个终端 app

properties 文件修改

如下:

# JDBC properties
spring.datasource.url=jdbc:mysql://localhost:3306/hb-01-one-to-one-uni
spring.datasource.username=springstudent
spring.datasource.password=springstudent
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

# Disable Hibernate usage of JDBC metadata
# not having this can resolve error
spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false

# turn of the banner and lower the logging level
spring.main.banner-mode=off
logging.level.root=warn

运行结果如下:

2025-03-17T17:58:18.452-04:00  WARN 62459 --- [demo-one-to-one-uni] [           main] org.hibernate.orm.deprecation            : HHH90000025: MySQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
Hello World!

Process finished with exit code 0

创建 entity

主要就是两个,InstructorDetail 对应 instructor_detail, Instructor 对应 instructor

  • instructor details

    package com.example.demo.entity;
    
    import jakarta.persistence.*;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.ToString;
    
    @Entity
    @Table(name = "instructor_detail")
    @Data
    @NoArgsConstructor
    @ToString
    public class InstructorDetail {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private int id;
    
        @Column(name = "youtube_channel")
        private String youtubeChannel;
    
        @Column(name = "hobby")
        private String hobby;
    
        public InstructorDetail(String youtubeChannel, String hobby) {
            this.youtubeChannel = youtubeChannel;
            this.hobby = hobby;
        }
    }
    
    
  • instructor

    package com.example.demo.entity;
    
    import jakarta.persistence.*;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.ToString;
    
    @Entity
    @Table(name = "instructor")
    @Data
    @NoArgsConstructor
    @ToString
    public class Instructor {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private int id;
    
        @Column(name = "first_name")
        private String firstname;
    
        @Column(name = "last_name")
        private String lastname;
    
        @Column(name = "email")
        private String email;
    
        // set up mapping to InstructorDetail
        @OneToOne(cascade = CascadeType.ALL)
        @JoinColumn(name = "instructor_detail_id")
        private InstructorDetail instructorDetail;
    
        public Instructor(String firstname, String lastname, String email) {
            this.firstname = firstname;
            this.lastname = lastname;
            this.email = email;
        }
    }
    
    

基本上没什么新的东西,除了下面这段:

      @OneToOne(cascade = CascadeType.ALL)
      @JoinColumn(name = "instructor_detail_id")
      private InstructorDetail instructorDetail;

具体内容最后补充

DAO & DAOimpl

这个的实现就比较简单了,因为只是做 demo,所以没打算分成几个文件去写,所有数据库的操作都会放在这里,并且调用 entityManager 中的方法去实现,而不会用 extend JpaRepository 的方法去实现

  • AppDAO

    package com.example.demo.dao;
    
    import com.example.demo.entity.Instructor;
    
    public interface AppDAO {
        void save (Instructor instructor);
    }
    
    
  • AppDAOImpl

      package com.example.demo.dao;
    
      import com.example.demo.entity.Instructor;
      import jakarta.persistence.EntityManager;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Repository;
    
    
      @Repository
      public class AppDAOImpl implements AppDAO{
          private final EntityManager entityManager;
    
          @Autowired
          public AppDAOImpl(EntityManager entityManager) {
              this.entityManager = entityManager;
          }
    
          @Override
          @Transactional
          public void save(Instructor instructor) {
              // it will also save instructor detail due to cascade
              entityManager.persist(instructor);
          }
      }
    
    

    ⚠️:这个 @Transactional 导入 spring 的就好,导入 jakara 的话,spring boot 就不会管理了

更新一下 main

主要是增加一下 logging 以及输出结果

package com.example.demo;

import com.example.demo.dao.AppDAO;
import com.example.demo.entity.Instructor;
import com.example.demo.entity.InstructorDetail;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Bean
	public CommandLineRunner commandLineRunner(AppDAO appDAO) {
		return runner -> {
			createInstructor(appDAO);
		};
	}

	private void createInstructor(AppDAO appDAO) {
		// create the instructor
		Instructor instructor = new Instructor("John", "Doe", "johndoe@gmail.com");
		InstructorDetail instructorDetail = new InstructorDetail("http://www.example.com", "Coding");

		// associate the objects
		instructor.setInstructorDetail(instructorDetail);

		// NOTE: this will ALSO save the details object because of CascadeType.ALL
		System.out.println("Saving instructor: " + instructor);
		appDAO.save(instructor);
		System.out.println("Done!");
	}
}

运行结果:

在这里插入图片描述

可以看到,这里具体执行了 2 条 queries,一个是写入 instructor,另一个是写入 instructor_details

数据库截图:

在这里插入图片描述

查询实例

这个操作很简单,主要修改三个文件:

  • DAO

    public interface AppDAO {
        Instructor findInstructorById(int id);
    }
    
  • DAOImpl

    @Repository
    public class AppDAOImpl implements AppDAO{
        @Override
        public Instructor findInstructorById(int id) {
            return entityManager.find(Instructor.class, id);
        }
    }
    
    
  • main

        @Bean
        public CommandLineRunner commandLineRunner(AppDAO appDAO) {
            return runner -> {
                findInstructor(appDAO);
            };
        }
    
        private void findInstructor(AppDAO appDAO) {
            int id = 1;
            System.out.println("Finding instructor id: " + id);
            Instructor instructor = appDAO.findInstructorById(id);
    
            System.out.println("Instructor: " + instructor);
            System.out.println("Associated Instructor Details: " + instructor.getInstructorDetail());
        }
    

效果如下:

在这里插入图片描述

删除实例

也是一样的操作,修改 3 个文件:

  • DAO

    public interface AppDAO {
    
        void deleteInstructorById(int id);
    }
    
  • DAOImpl

        @Override
        @Transactional
        public void deleteInstructorById(int id) {
            Instructor instructor = this.findInstructorById(id);
    
            if (instructor != null) {
                entityManager.remove(instructor);
            }
        }
    
  • main

        @Bean
        public CommandLineRunner commandLineRunner(AppDAO appDAO) {
            return runner -> {
                deleteInstructor(appDAO);
            };
        }
    
        private void deleteInstructor(AppDAO appDAO) {
            int id = 2;
            System.out.println("Deleting instructor id: " + id);
            appDAO.deleteInstructorById(id);
            System.out.println("Done!");
        }
    

结果如下:

在这里插入图片描述

one-to-one bi-directional

这个修改其实没必要动数据库,等到之后捋一遍就明白为什么了

更新查询实例

public class InstructorDetail {
    // ...

    // updated code
    @OneToOne(mappedBy = "instructorDetails", cascade = CascadeType.ALL)
    private Instructor instructor;

    // ...

    // remove @ToString annotation as null field(private Instructor instructor;) will throw error
    @Override
    public String toString() {
        return "InstructorDetail: id = " + this.getId() + ", youtubeChannel: " + this.getYoutubeChannel()
                + ", hobby: " + this.getHobby() + ".";
    }
}

@toString 空值会造成 lombok 的一些问题,在这个 ticket 有提到:@ToString formatting ‘language’. #1297。这里用 getId() 比较合适,否则的话会造成循环调用,形成无止尽的递归

DAO 和 DAOImpl 更新

主要是获取新的 InstructorDetails,代码比较简单:

public interface AppDAO {
    InstructorDetail findInstructorDetailById(int id);
}

    @Override
    public InstructorDetail findInstructorDetailById(int id) {
        return entityManager.find(InstructorDetail.class, id);
    }

更新 main 方法

其实就是新增一个方法,去获取输出通过 findInstructorDetailById 获取的实例:

	@Bean
	public CommandLineRunner commandLineRunner(AppDAO appDAO) {
		return runner -> {
			findInstructorDetail(appDAO);
		};
	}

	private void findInstructorDetail(AppDAO appDAO) {
		// find the instructor detail object
		int id = 1;
		InstructorDetail instructorDetail = appDAO.findInstructorDetailById(id);
		System.out.println("Instructor Detail:" + instructorDetail);

		// print the associated instructor
		System.out.println("Associated instructor: " + instructorDetail.getInstructor());
		System.out.println("Done.");
	}

输出结果

在这里插入图片描述

删除实例更新

  • DAO

    void deleteInstructorDetailById(int id);
    
  • DAO Impl

        @Override
        @Transactional
        public void deleteInstructorDetailById(int id) {
            InstructorDetail instructorDetail = this.findInstructorDetailById(id);
    
            if (instructorDetail != null) {
                entityManager.remove(instructorDetail);
            }
        }
    
  • main

      @Bean
      public CommandLineRunner commandLineRunner(AppDAO appDAO) {
        return runner -> {
          deleteInstructorDetail(appDAO);
        };
      }
    
      private void deleteInstructorDetail(AppDAO appDAO) {
        int id = 1;
        System.out.println("Deleting instructor id: " + id);
        appDAO.deleteInstructorDetailById(id);
        System.out.println("Done!");
      }
    

结果:

在这里插入图片描述

只删除 instructor detail 但是不删除 instructor

这个方式可以通过修改 CascadeType 去实现,如:

    @OneToOne(mappedBy = "instructorDetail", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    private Instructor instructor;

修改 DAO Impl 去手动移除关联:

    @Override
    @Transactional
    public void deleteInstructorDetailById(int id) {
        InstructorDetail instructorDetail = this.findInstructorDetailById(id);

        // remove the associated object reference, break the bi-directional link
        instructorDetail.getInstructor().setInstructorDetail(null);

        entityManager.remove(instructorDetail);
    }

最后运行结果:

在这里插入图片描述

在这里插入图片描述

建立关联的注解

@JoinColumn

这个代表的是当前属性为 foreign key,并且可以通过当前的 foreign key 寻找到对应的实例

需要注意的是, @JoinColumn 无法单独使用,必须要搭配对应的关系——@OneToOne@OneToMany@ManyToMany 才能够正确工作

@OneToOne

这个注解只代表了当前的属性与当前的 entity 存在 1-to-1 的对应关系,参考两种用法:

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "instructor_detail_id")
    private InstructorDetail instructorDetail;

// =====================================================

    @OneToOne(mappedBy = "instructorDetails", cascade = CascadeType.ALL)
    private Instructor instructor;

前者可以直接通过 foreign key 寻找对应的关系,写成 query 大体如下:

select * from instructor_detail where id = 1;

后者的 query 大体如下:

SELECT * FROM instructor_detail d
JOIN instructor i ON d.id = i.instructor_detail_id
WHERE d.id = 1;

对比起来的话,前者因为直接用 foreign key 去找,不用调用一个 join,所以表现上会稍微快一些

reference

  • mysql 权限问题

    这个取决于 properties 文件中使用的用户,如果不是 root,那么可能就会有无法访问的问题,这个时候跑一下下面的脚本就行了,用户名和 db 名称用数据库中的代替:

    mysql -u root -p
    USE dbname;
    
    # 如果用户不存在,直接 grant 会报错
    CREATE USER 'username'@'%' IDENTIFIED BY 'your_password';
    GRANT ALL PRIVILEGES ON `hb-01-one-to-one-uni`.* TO 'username'@'%';
    
    FLUSH PRIVILEGES;
    
    SHOW GRANTS FOR 'username'@'%';
    
  • properties 文件配置

    来自官方的 repo: spring-lifecycle-smoke-tests


http://www.kler.cn/a/594250.html

相关文章:

  • JVM OOM问题如何排查和解决
  • Python连接数据库进行增删改查
  • 【Prometheus】prometheus标签替换label_replace,动态修改生成标签,增强查询的灵活性和表达能力
  • Unity 使用Odin插件解决多层字典配置文件问题
  • 嵌入式4-Modbus
  • 单片机flash存储也做磨损均衡
  • 【AI】在AWS AI芯片服务上部署运行Qwen 2.5模型
  • 3. 轴指令(omron 机器自动化控制器)——>MC_SetOverride
  • 使用LangChain实现基于LLM和RAG的PDF问答系统
  • fetch,ajax,axios的区别以及使用
  • Three.js世界中的三要素:场景、相机、渲染器
  • 华为IPD集成产品开发
  • 数据分析的12个挑战及其解决方法
  • spring boot maven一栏引入本地包
  • 个人陈述本人于2011年8月被XXX大学经济学专业录取
  • Excel知识库与LLM结合的解决方案详细分析
  • 【视频】H.264的码率和图像质量
  • Redis 本地安装
  • Docker Swarm集群搭建
  • HarmonyOS第27天:鸿蒙开发新征程探索未来,持续进阶