Java EE--框架篇(3-2)Hibernate

目录

前言

快速入门Hibernate(SpringBoot集成Hibernate)

***:没有任何Hibernate出现,为什么叫集成Hibernate?

***:Jpa、Spring data jpa和Hibernate之间有什么关系?

***:JPA规范之主键策略

***:JPA规范之数据库表行为

单表增删改查

***:getOne()和findById()的区别?

单表条件查询

***:如何返回自定义类型?

多表联合查询(对象导航查询)

***:一对多,对象导航保存时,可以仅仅调用“多”的一方的保存方法吗?

***:执行查询为什么还要添加@Transaction注解,开启事务?

***:mysql的外键关联策略有哪些?hibernate框架自动建表时,设置的外键关联策略是什么?

分页查询

批量操作

***:SQL、QBC、QBE、HQL的概念和优缺点对比

***:什么是一级缓存,什么是二级缓存?有什么作用?

***:MyBatis和Hibernate优缺点对比


前言

带着问题学java系列博文之java基础篇。从问题出发,学习java知识。


书接上文,本篇继续讲解Hibernate框架,并对Mybatis和Hibernate两个框架做一个对比总结。

快速入门Hibernate(SpringBoot集成Hibernate)

项目结构图:

实体类:

@Entity //声明是一个实体类
@Table(name = "student") //关联数据库表
@Data
public class Student {

    @Id  //声明主键
    @GeneratedValue(strategy = GenerationType.IDENTITY) //配置主键生成策略
    private Integer id;

    @Column(name = "name") //声明对应表中的列名,如果属性名和列名一致,可以省略不写
    private String name;
    private Integer age;
    private String address;

    @OneToOne(mappedBy = "student",cascade = CascadeType.ALL)
    private Grades grades;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                ",grades="+grades.toString()+
                '}';
    }
}


@Entity
@Table(name = "grades")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Grades implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private Integer math;
    private Integer chinese;
    private Integer english;

    @OneToOne(targetEntity = Student.class,fetch = FetchType.EAGER)
    @JoinColumn(name = "student_id",referencedColumnName = "id")
    private Student student;

    @Override
    public String toString() {
        return "Grades{" +
                "id=" + id +
                ", math=" + math +
                ", chinese=" + chinese +
                ", english=" + english +
                '}';
    }
}

repository:

@Repository
public interface GradesRepository extends JpaRepository<Grades,Integer>, JpaSpecificationExecutor<Grades> {
}

@Repository
public interface StudentRepository extends JpaRepository<Student,Integer>, JpaSpecificationExecutor<Student> {
}

启动类添加注解开启repository:

@SpringBootApplication
@EnableJpaRepositories
public class LearnhibernateApplication {
    public static void main(String[] args) {
        SpringApplication.run(LearnhibernateApplication.class, args);
    }
}

配置信息:

##配置数据库表行为
spring.jpa.hibernate.ddl-auto=update
##打印sql语句
spring.jpa.show-sql=true
##配置数据库
spring.jpa.database=mysql
##配置方言
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect
##命名策略(支持下划线:即列名student_id,属性名studentId)
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/school?useSSL=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=****

所需依赖:

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

至此,SpringBoot集成Hibernate完毕,依赖Spring data JPA,实现dao层接口类,通过Repository接口类可以直接进行基本的单表操作和一般的复杂条件查询(分页)。要学习Hibernate,建议先学习jpa规范,然后入门hibernate纯xml配置阶段,最后进入全注解阶段。本例是在spring data jpa基础上,使用全注解,实际开发推荐使用。

***:没有任何Hibernate出现,为什么叫集成Hibernate?

确实上例从始至终没有提到任何Hibernate,那为什么还叫做集成Hibernate呢?我们来看下引入的依赖,主要有一个spring-boot-starter-data-jpa,打开maven,具体看下它包含哪些依赖:

从上图可以看到,原来这个data-jpa中包含了hibernate-core(如上图红框)。

***:Jpa、Spring data jpa和Hibernate之间有什么关系?

上例集成Hibernate可以看到,通篇都是使用jpa规范,使用jpa规范定义的注解进行实体类的编写(区别于mybatis,主键使用的是mybatis定义的注解,也不需要在类上注解Entity,实体类的扫描交给mybatis(在启动类上添加扫描路径注解)),实体类扫描完全交给SpringBoot框架;然后repository接口也是继承的JpaRepository和JpaSpecificationExecutor;最后配置也全部都是spring.jpa打头。所以整体来说,其实上例都是jpa操作,hibernate只是jpa的底层实现。

JPA 即Java Persistence API。JPA 是一个基于O/R映射的标准规范(目前最新版本是JPA 2.1 ),JPA通过JDK 5.0注解XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。也就是说JPA是java为了规范化orm框架的实现,而制定的一套规范(接口和抽象类),至于实现就交给具体的ORM框架。

JPA 的主要实现有HibernateEclipseLink 和OpenJPA 等,这也意味着我们只要使用JPA 来开发,底层实现的框架不影响我们应用上层的使用。

Spring data jpa则是spring框架对jpa的集成和封装,比如接管实体类扫描、repository的实例化、数据库连接池、配置信息等,更加简化我们的编程。

综上,可以看到spring data jpa 是spring框架对jpa的集成封装,jpa是java对orm框架的抽象规范,hibernate是jpa规范的底层实现。

 

***:JPA规范之主键策略

jpa定义了一个注解用于配置实体类的主键生成策略:@GeneratedValue;

package javax.persistence;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GeneratedValue {
    GenerationType strategy() default GenerationType.AUTO;

    String generator() default "";
}

可以看到注解有两个可设置属性:generator和strategy;其中strategy用于设置生成策略,generator用于配置序列名称。

strategy不配置时,默认值GenerationType.AUTO,共有如下可供配置的选择:

GenerationType.AUTO:由程序自动选择主键生成策略(框架自动根据数据库和运行环境选择最合适的主键策略)

GenerationType.IDENTITY: 自增(要求底层数据库支持主键自增,比如Mysql支持)

GenerationType.TABLE: jpa提供的一种机制,通过一张数据库表(存储下一条记录的主键值)的形式实现主键自增(即不再依赖数据库支持)

GenerationType.SEQUENCE: 序列(要求底层数据库支持序列,比如Oracle支持)

实际业务开发时建议根据选用的数据库,配置合适的主键策略。

generator 主要是为了SEQUENCE策略准备的,它常常和@SequenceGenerator配合使用。比如:

在实体类上:

//声明一个sequence,名称为studentSEQ

@SequenceGenerator(name = "studentSEQ",sequenceName = "studentSEQ")

在主键属性上:

//配置主键生成策略是SEQUENCE,sequence的名称是上面声明的name为studentSEQ

@GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "studentSEQ")

***:JPA规范之数据库表行为

上例我们没有像集成mybatis时那样,先创建数据库表,而是在配置时配置了:

spring.jpa.hibernate.ddl-auto=update

这个配置就是用来设定orm框架具体的数据库表行为,共有如下可供配置的选择:

update 运行时,看数据库是否有表,没有则创建,有则不创建;

create 运行时每次都是新建表(如果已有表,则先删除表)

create-drop 运行时创建数据库表,结束session时,销毁表

none 无表行为(当数据库没有表时,操作表将会报错)

validate 验证表是否有变化,确保表一致

这里也体现了Hibernate相较于mybatis的一个大优势,hibernate可以有表行为,可以自动建表

 

单表增删改查

@SpringBootTest
@RunWith(SpringRunner.class)
public class GradesRepositoryTest {

    @Autowired
    private GradesRepository repository;
    @Autowired
    private StudentRepository studentRepository;

    @Test
    public void testRepository(){
        List<Grades> grades = repository.findAll();
        for (Grades grade : grades) {
            System.out.println(grade);
        }
    }
}

和mybatis的通用mapper一样,spring data jpa封装的JpaRepository和JpaSpecificationExecutor已经实现了单表操作的增删改查系列方法,我们只需要直接使用即可。下面梳理一下repository的已有方法:

1.添加方法(兼具更新功能)

save():当传入的实例没有主键(或者主键值数据库中没有重复的)时,则向数据库添加一条记录(执行insert),主键根据配置的主键策略动态生成;当传入的实例有主键,并且数据库中有重复的,则执行更新操作(执行update);根据日志,可以看到其实一个save方法,保存有主键值的实例时,实际执行了两条sql(一条select,一条实际业务sql(insert或者update));

saveAll():一次保存多条数据。它本质和save()方法一样,每保存一条记录也是实际执行两条sql(有主键的实例),循环执行而已,效率是很低的。批量操作请继续往下看;

saveAndFlush():和save()方法功能相同,区别是立即生效。

2.删除方法

delete():要注意这个方法,并不是条件删除,也就是说如果传参是一个没有主键值的实体,那delete将不会做任何操作,但是如果传入主键id的话,那还不如直接调用deleteById方法呢。

deleteAll():删除表中所有记录,先执行select,查询所有记录,然后依次执行delete by id,效率低;

deleteAll(list):一次删除多条记录,本质是循环执行,拿到主键id,执行select查询,找到则执行delete,依次循环,遍历整个list;

deleteAllInBatch():批量删除表中所有数据,这里其实执行的是“delete from table”,一条语句删除所有,批量操作效率高;

deleteInBatch(list):批量删除多条数据,区别于deleteAll,这里使用优化的sql(delete from table where id=?or id = ?or id=?...),一条语句删除所有,效率更高;

deleteById():根据主键删除记录;

3.查询方法

find系列方法:

findAll():查询所有;

findAll(Sort):根据传入的Sort,对查询结果进行排序;

findAll(Example):单表条件查询;具体使用请继续往下看;

findById():根据主键id查询;

findOne():根据条件查找,注意要保证吻合条件的记录只有一条,否则将会报错;

getOne():根据主键查询;

***:getOne()和findById()的区别?

要说这两个的区别,先了解两个概念:延迟加载和立即加载。

延迟加载:查询数据等操作,不是立即操作数据库,而是等实际用到对应数据时才真正发送sql语句,执行具体操作;

立即加载:查询数据等操作,是立即发送sql语句,执行数据库操作。

尤其在对象导航查询时,当查询一对多对象时,这个对应的“多”对象就是默认延迟加载,只有真正用到它们时,才会实际操作数据库查询。

getOne()方法就是立即加载,findById()方法就是延迟加载,而且find系列方法都是延迟加载。

 

单表条件查询

1.单表属性条件查询(使用默认方法)

public interface PeopleRepository extends JpaRepository<People,Integer>, JpaSpecificationExecutor<People> {
    List<People> findAllByAddressLikeAndSexOrderByAge(String address,String sex);
}

如上,只需要在repository中直接写方法即可,支持实体类属性的基本查询(= ,like,after、before、between、in、within、endwith、startwith、contains),连接符(and、or),排序(orderby属性Desc/asc)。

find开头方法就是默认的find系列方法,延迟加载;get开头方法就是立即加载;query开头相当于find。

 

2.单表属性条件查询(使用Example,也可以使用Specification,详见分页查询的举例)

@Test
@Transactional
@Rollback(false)
public void testRepository(){
        //设置条件值
        People people = new People();
        people.setAge(18);
        people.setSex("女");
        people.setAddress("球");
        //设置匹配规则
        ExampleMatcher matcher = ExampleMatcher.matching()
                //设置address字段匹配规则是以“球”为结尾的
                .withMatcher("address",ExampleMatcher.GenericPropertyMatchers.endsWith())
                //忽略age字段,即age不作为查询条件
                .withIgnorePaths("age");
        Example<People> example = Example.of(people,matcher);

        //没有设置匹配规则的属性,则默认是 =,即 sex = “女”
        List<People> all = peopleRepository.findAll(example);
        for (People people1 : all) {
            System.out.println(people1);
        }
}

对比mybatis,如果是简单的条件查询,mybatis的通用mapper支持直接传入一个实体,实体属性值就是条件查询值;而jpa则需要通过Example对象或者编写对应方法才可以。

3.使用HQL(JPQL)查询

当1.2两种框架默认实现的方式无法满足我们的需求时,还可以通过手动编写HQL实现查询:

public interface PeopleRepository extends JpaRepository<People,Integer>, JpaSpecificationExecutor<People> {
    @Query(value = "from People where id = ?1 and address like ?2")
    People findHQL(Integer id,String addressLike);
}

4.使用SQL查询

类似3,也可以写sql:注意注解增加 nativeQuery=true(默认是false,使用HQL)

public interface PeopleRepository extends JpaRepository<People,Integer>, JpaSpecificationExecutor<People> {

    @Query(value = "from People where id = ?1 and address like ?2")
    People findHQL(Integer id,String addressLike);

    @Query(value = "select * from people where id = ? and address like ?",nativeQuery = true)
    People findSql(Integer id,String addressLike);
}

***:如何返回自定义类型?

jpa规范没有定义自动封装结果转成自定义类型,因此相较于Mybatis的自定义类型结果返回,只需要直接写类型,框架实现自动封装;依赖hibernate实现的jpa,则需要修改一下HQL,明确调用自定义类型的构造器。具体如下例:

public interface PeopleRepository extends JpaRepository<People,Integer>, JpaSpecificationExecutor<People> {

    @Query("select new com.zst.learnhibernate.dto.PeopleDto(p.address,p.sex,p.age) from People p where id = ?1")
    PeopleDto findDtoById(Integer id);
}


@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class PeopleDto implements Serializable {
    private String address;
    private String sex;
    private Integer age;
}

***:注意这里其实就是明确调用自定义类型的构造器,所以首先要确保有对应的构造器,其次要确保传参的顺序和构造器参数顺序一致。另外为了避免框架找不到自定义类型,最好是写全类名。

当然,还有别的方式可以实现自定义类型。比如,自定义查询方法的返回类型是List<Object[]>,不做封装,框架返回的就是这个类型。然后自己实现类型转换,将List<Object[]>的数据封装成对应的自定义类型。这些方式都不如直接在HQL中指定构造器来的简单,所以推荐使用上例的方式。

 

多表联合查询(对象导航查询)

使用HQL或者SQL,在repository中实现自定义方法,可以实现多表联合查询,这和Mybatis是一样的。优于Mybatis这种半自动的ORM框架,hibernate框架属于全自动的ORM框架,支持对象导航查询,适用于一对一、一对多、多对多等关系模型。

一对一:(学生和成绩,一个学生对应一条成绩,这是为了举例,实际业务开发,建议优化设计,将两张表合成一张表,没有必要做成两张表)

一对多:(一个班级有多名学生)

1.在实体类上配置关联关系(注意一定要配置外键的关联策略(我们这里配置的是级联ALL,相当于mysql的cascade),否则框架将不会按照对象导航操作执行)

@Entity //声明是一个实体类
@Table(name = "student") //关联数据库表
@Data
public class Student {

    @Id  //声明主键
    @GeneratedValue(strategy = GenerationType.IDENTITY) //配置主键生成策略
    private Integer id;

    @Column(name = "name") //声明对应表中的列名,如果属性名和列名一致,可以省略不写
    private String name;
    private Integer age;
    private String address;

    @ManyToOne(targetEntity = Class.class)
    @JoinColumn(name = "class_id",referencedColumnName = "id")
    private Class aClass;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

@Data
@Entity
@Table(name = "class")
public class Class implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String className;
    private String classTeacher;
    private String floor;

    @OneToMany(mappedBy = "aClass",cascade = CascadeType.ALL)
    private List<Student> studentList;

    @Override
    public String toString() {
        return "Class{" +
                "id=" + id +
                ", className='" + className + '\'' +
                ", classTeacher='" + classTeacher + '\'' +
                ", floor='" + floor + '\'' +
                '}';
    }
}

2.测试对象导航系列操作

@SpringBootTest
@RunWith(SpringRunner.class)
public class ClassTest {

    @Autowired
    private ClassRepository classRepository;
    @Autowired
    private StudentRepository studentRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void testSave(){
        Class threeClass = new Class();
        threeClass.setClassName("三班");
        threeClass.setClassTeacher("徐老师");
        threeClass.setFloor("6楼");

        Student a = new Student();
        a.setName("张三");
        a.setAge(16);
        a.setAddress("肥东");
        //设置学生与班级的关系
        a.setAClass(threeClass);

        Student b = new Student();
        b.setName("李四");
        b.setAge(16);
        b.setAddress("肥西");
        //设置学生与班级的关系
        b.setAClass(threeClass);

        Student c = new Student();
        c.setName("王五");
        c.setAge(16);
        c.setAddress("瑶海");
        //设置学生与班级的关系
        c.setAClass(threeClass);

        List<Student> studentList = new ArrayList<>();
        studentList.add(a);
        studentList.add(b);
        studentList.add(c);
        //设置班级与学生的关系
        threeClass.setStudentList(studentList);

        classRepository.save(threeClass);
    }

    @Test
    @Transactional
    public void testQuery(){
        Optional<Class> threeClass = classRepository.findById(1);
        if (threeClass.isPresent()){
            System.out.println(threeClass.get());
            for (Student student : threeClass.get().getStudentList()) {
                System.out.println(student);
            }
        }
    }

    @Test
    @Transactional
    @Rollback(false)
    public void testDelete(){
        classRepository.deleteById(1);
    }

}

执行testSave():

虽然只是调用classRepository.save(class),实际却执行了4条insert,与class关联的3个student也被保存了,并且还自动设置了外键。这就是因为我们在实体类上配置了关联关系,并且配置了级联策略是ALL(即所有操作都同步,相当于mysql数据库的cascade策略),框架在执行时自动识别了关联关系,并进行对应的映射操作,全自动的ORM(对象-关系-映射)就体现在这里。

***:一对多,对象导航保存时,可以仅仅调用“多”的一方的保存方法吗?

答案是绝对不可以。其实如果理解了就很简单,一对多关系,“多”的一方,属于外键关联“一”的主键,如果先调用“多”的一方的保存方法,那它外键关联的主键将找不到,因为此时“一”的一方还没保存呢。这就会导致保存失败,方法调用报错。所以如果是一对多关系,保存时一定要先执行“一”的一方的保存方法(要预先设置好实例的关联对象,且双方都要设置,比如上例中threeClass一定要设置studentList属性,每个student也都要设置aClass属性),而“多”的一方将会被框架识别,并自动保存(因此,“多”的一方的保存方法完全没必要调用)。

执行testQuery():

***:执行查询为什么还要添加@Transaction注解,开启事务?

这里有个小细节,我们明明只是执行查询方法,可是却要在方法上添加@Transaction注解,让spring框架开启事务。按理说,查询不涉及数据写操作,而且就一次查询,理论上是不需要开启事务的。但是实际如果我们这里不开启事务的话,方法执行将会报错:

“org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.zst.learnhibernate.domain.Class.studentList, could not initialize proxy - no Session”

从错误信息可以看到,这是hibernate框架抛出的错误,而且还有LazyInitlization字样,这是hibernate框架的延迟加载(懒加载)吗?确实,这里就是因为Hibernate框架对这种关联关系的对象导航查询,默认都是采取延迟加载的策略(即使调用getOne()方法查询,也是延迟加载。除非在实体类上配置关联关系时,明确指定了立即加载),只有实际用到的时候,才会真正发送查询语句,获取数据。从执行结果也可以看到,首先日志打印了一条select语句,查询了id为1的class,并打印了class;然后发现需要用到studentList,所以再查询一次,打印了第二条select语句,查询出所有关联的学生数据。

因为延迟加载策略,所以这里虽然看起来只是一条查询语句,但实际上却是查询了两次数据库,且两次查询是有前后时间间隔的。所以,首先这里要求数据库连接session没有关闭,否则第一次查询执行完毕就关闭了session,那第二次执行查询肯定会报错;其次两次查询期间数据有可能被其他用户修改,为了保证数据的准确性(不存在脏读),所以要对数据进行加锁,使用事务包裹,可以保证数据的一致性,避免脏读出现。

***:小贴士(外键关联,设置立即加载范例)

    //配置外键关联策略,加载策略FetchType.EAGER:立即加载
    //fetch 不配置,默认是FetchType.LAZY:延迟加载
    @ManyToOne(targetEntity = Class.class,cascade = CascadeType.ALL,fetch = FetchType.EAGER)
    @JoinColumn(name = "class_id",referencedColumnName = "id")
    private Class aClass;

***:mysql的外键关联策略有哪些?hibernate框架自动建表时,设置的外键关联策略是什么?

mysql的外键关联策略有4种:

cascade:级联,即主表update/delete时,同步update/delete子表关联的相关记录;

set null:设置为空,即主表update/delete时,同步将子表关联的相关记录的外键设置为null;注意这个要求子表的外键允许为空;

no action:没有任何动作,如果子表中存在与主表关联的记录,则不允许对主表的对应记录进行update/delete操作;

restrict:和no action类似,都是立即检查关联关系。

jpa规范制订了以下几种外键关联策略(在CascadeType类中):

ALL:包含所有,级联保存、更新、删除;

PERSIST:同步新增(保存);

MERGE:同步保存、更新;

REMOVE:同步删除;

REFRESH:同步刷新,相当于当刷新主表时,从表也同步刷新;

DETACH:Cascade detach operation,级联脱管/游离操作。如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。(不明白)

***:有个小问题,当我设置关联策略是ALL时,hibernate框架自动建表,我发现从表的外键策略居然是RESTRICT(上例中的student的class_id外键策略),难道不应该是cascade吗?欢迎知情大佬为我解惑。(博主的环境是:2.2.0的spring-boot-starter-data-jpa,8.0的mysql)

 

多对多:(hibernate框架支持多对多和一对多具有相同的对象导航操作,这里不再赘述;一门课程可以被多个学生选修,一个学生可以选修多门课程)

@Entity
@Table(name = "student") 
@Data
public class Student {

    @Id  //声明主键
    @GeneratedValue(strategy = GenerationType.IDENTITY) //配置主键生成策略
    private Integer id;

    @Column(name = "name") //声明对应表中的列名,如果属性名和列名一致,可以省略不写
    private String name;
    private Integer age;
    private String address;

    @ManyToMany(targetEntity = Subject.class,cascade = CascadeType.ALL)
    //多对多其实就是借助中间表维护外键关联关系,所以这用joinTable
    @JoinTable(name = "tb_student_subject",
            //配置当前对象在中间表中的外键
            joinColumns = {@JoinColumn(name = "ss_student_id",referencedColumnName = "id")},
            //配置对方对象在中间表中的外键
            inverseJoinColumns = {@JoinColumn(name = "ss_subject_id",referencedColumnName = "id")})
    private Set<Subject> subjects;
}


@Data
@Entity
@Table(name = "subject")
public class Subject implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String teacher;
    private Integer classHour;
    
    //放弃外键维护权,交给student维护,一般由主动的一方维护
    @ManyToMany(mappedBy = "subjects")
    private Set<Student> students;
}

 

分页查询

spring data jpa实现的JpaSpecificationExecutor,自带了分页查询方法,我们可以直接使用。

    @Test
    @Transactional
    public void testQueryByPage(){
        //简单的分页查询
        Pageable pageable = PageRequest.of(0,2);
        Page<People> peoplePage = peopleRepository.findAll(pageable);
        System.out.println("表中数据总数:"+peoplePage.getTotalElements());
        System.out.println("分页总页数:"+peoplePage.getTotalPages());
        for (People people : peoplePage.getContent()) {
            System.out.println(people);
        }

        //带条件的分页查询
        Specification<People> spec = new Specification<People>() {
            @Override
            public Predicate toPredicate(Root<People> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                //得到查询属性
                Path<Object> address = root.get("address");
                Path<Object> age = root.get("age");
                Path<Object> sex = root.get("sex");
                //设置条件
                //模糊匹配地址like "%国%"
                Predicate addressLike = criteriaBuilder.like(address.as(String.class), "%国%");
                //年龄小于等于20
                Predicate ageLessOrEqual = criteriaBuilder.lessThanOrEqualTo(age.as(Integer.class), 20);
                //性别等于“男”
                Predicate sexEqual = criteriaBuilder.equal(sex, "男");
                Predicate and = criteriaBuilder.and(addressLike, ageLessOrEqual, sexEqual);
                return and;
            }
        };
        Page<People> page = peopleRepository.findAll(spec, pageable);
        System.out.println("表中数据总数:"+page.getTotalElements());
        System.out.println("分页总页数:"+page.getTotalPages());
        for (People people : page.getContent()) {
            System.out.println(people);
        }
    }

区别于Mybatis自带的分页查询方法,hibernate实现的分页方法不需要自己计算limit 参数,只需要传入一个页码,每页条数即可。且hibernate实现的查询也是类似PageHelper实现的一样,先查count,然后查询limit,是效率高的分页查询,所以可以直接使用。有个小缺点就是,这里的页码是从0开始的,而我们界面页码一般都是从1开始,这个只能是与前端约定好,或者后端默认将传入的页码参数+1。

 

批量操作

要想高效率的实现批量操作,除了自己编写sql之外,hibernate还支持开启批量操作,而避免丑陋的for循环遍历。要想实现支持批量操作,需要两步,范例如下:

1.开启配置(学习过xml配置阶段就知道,其实是设置初始化JpaVendorAdapter的参数)

##开启批量操作支持
#单次支持批量操作的最大数量
spring.jpa.properties.hibernate.jdbc.batch_size=500
#保证hibernate低版本支持批量操作(Hibernate5.0以后默认true)
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true
#支持批量插入
spring.jpa.properties.hibernate.order_inserts=true
#支持批量更新
spring.jpa.properties.hibernate.order_updates=true

2.利用实体管理器实现批量操作

    //通过注解拿到实体类管理器
    @PersistenceContext
    private EntityManager em;

    @Test
    @Transactional
    @Rollback(false)
    public void testBatchInsert(){
        for (int i = 0; i < 10; i++) {
            People people = new People();
            people.setAge(21);
            people.setAddress("中国");
            people.setSex("男");
            em.persist(people);
            //虽然设置单次最大支持批量操作500条,但是还是建议积累一定数据时就刷新到数据库一次,
            // 可以有效减少内存占用,也可以防止单次操作超过设置的数量上限
            if ( i % 100 == 0){
                em.flush();
                em.clear();
            }
        }
        //将剩余的数据刷新到数据库
        em.flush();
        em.clear();
    }

    @Test
    @Transactional
    @Rollback(false)
    public void testBatchUpdate(){
        List<People> peopleList = peopleRepository.findAll();
        for (int i = 0; i < peopleList.size(); i++) {
            People people = peopleList.get(i);
            people.setAddress("新中国");
            em.merge(people);
            if (i % 100 == 0){
                em.flush();
                em.clear();
            }
        }
        em.flush();
        em.clear();
    }

 

***:SQL、QBC、QBE、HQL的概念和优缺点对比

SQL(Structured Query Language):结构化查询语言,是一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系型数据库。

QBC(Query By Criteria):API提供了检索对象的另一种方式,它主要由Criteria接口、Criterion接口和Expresson类组成,它支持在运行时动态生成查询语句。

QBE(Query By Example):即实例查询语言。它是一种基于图形的点击式查询数据库的方法。

HQL(Hibernate Query Language):Hibernate 查询语言(HQL)是一种面向对象的查询语言,类似于 SQL,但不是去对表和列进行操作,而是面向对象和它们的属性。 HQL 查询被 Hibernate 翻译为传统的 SQL 查询从而对数据库进行操作。HQL和JPQL非常相似。

优缺点对比:SQL不用多说,所有的数据库操作归根结底,都是sql语句,非常灵活,可以实现你想要的任何数据库操作;缺点就是与数据库强相关,不同的数据库支持的sql语法不同。QBC和QBE在编码角度来说,有点类似,都是新建一个条件对象,然后进行查询,编码上都有一点复杂。HQL是由Hibernate框架支持实现,特点是拥有类似于SQL的灵活性,还避免了SQL的不适应数据库变化的缺点,所以推荐使用HQL。

 

***:什么是一级缓存,什么是二级缓存?有什么作用?

Mybatis的一级缓存是默认开启的,它是相对于同一个SqlSession而言的,在一次session内,当多次查询的参数和sql完全相同时,实际上只在第一次查询时发送sql,操作数据库;后面的查询都是先从缓存中获取。

    @Test
    @Transactional
    public void testCache(){
        People people = peopleMapper.selectByPrimaryKey(1);
        System.out.println(people);

        People people1 = peopleMapper.selectByPrimaryKey(1);
        System.out.println(people1);
    }
    
    @Test
    public void testNoCache(){
        People people = peopleMapper.selectByPrimaryKey(1);
        System.out.println(people);

        People people1 = peopleMapper.selectByPrimaryKey(1);
        System.out.println(people1);
    }

看两次执行的截图:

testNoCache():

testCache():

可以看到虽然两个方法的代码完全一样,但是testNoCache()方法实际却是查询了两次数据库,先后发送了两条查询sql;而testCache()只发送了一条sql,查询了一次数据库。二者的区别主要是因为testCache()添加了@Transaction注解,开启了事务,方法内的所有查询都是使用同一个session(相当于JDBC编程时,用同一个connection,进行多次数据库查询)。刚刚我们分析了Mybatis默认开启了一级缓存,对同一session内,相同参数和sql的多次查询进行了缓存,后续的重复查询将直接从缓存中获取,这里的执行结果也充分验证了这一理论。

Mybatis的二级缓存则是需要我们手动开启并配置,它不像一级缓存只能存储在本地内存,可以自由配置存储位置。Mybatis的二级缓存是指mapper映射文件,二级缓存是多个sqlSession共享的,其作用域是mapper下的同一个namespace。在不同的sqlSession中,相同的namespace下,相同的查询sql语句并且参数也相同的情况下,会命中二级缓存。如果调用相同namespace下的mapper映射文件中的增删改SQL,并执行了commit操作,此时会清空该namespace下的二级缓存。可以简单的理解为,mybatis框架缓存了每次发生的数据库操作,使用一个Map存储(key:sql,value:Object(对象实体))。因为要将对象实体存储起来(不一定是内存,可以是redis等其他缓存介质),所以要求实体类必须实现Serializable接口,支持序列化。

Mybatis二级缓存简单示意图

springboot集成mybatis开启二级缓存示例:

在配置中开启二级缓存(其实新版springboot默认值就是true)
##开启二级缓存
mybatis.configuration.cache-enabled=true
// 添加命名空间注解
// 相当于给当前mapper开启二级缓存
@CacheNamespace
public interface PeopleMapper extends Mapper<People> {
}

@SpringBootTest
@RunWith(SpringRunner.class)
public class StudentMapperTest {
    @Test
    public void testSecondLevelCache(){
        People people = peopleMapper.selectByPrimaryKey(1);
        System.out.println(people);

        People people1 = peopleMapper.selectByPrimaryKey(1);
        System.out.println(people1);
    }
}

执行截图如下:

可以看到使用peopleMapper进行查询时,每次都是先去二级缓存中查找,找不到则查询数据库,找到则直接返回。所以这里第二次查询就没有操作数据库,而是直接从缓存返回。示例仅仅演示了开启本地存储的二级缓存,mybatis还支持使用redis、memcache等缓存介质作为存储的二级缓存,有兴趣可以深入了解具体配置开启。

 

Hibernate的一级缓存或者说JPA定义的一级缓存规范是对于EntityManager而言的,和Mybatis一样也是一个session范围内有效,框架默认开启,缓存保存在内存中。同样的可以参照上面mybatis的范例,测试repository查询,当添加@transaction注解时,一级缓存发挥作用。

Hibernate的二级缓存也是需要手动开启的,它类似于Mybatis的基于mapper,hibernate是基于实体,sqlSession间有效。

Springboot集成hibernate开启二级缓存示例:

##开启二级缓存
#打开二级缓存
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
#开启查询二级缓存
spring.jpa.properties.hibernate.cache.use_query_cache=true
#指定二级缓存的实现类
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory

这里由于用到了EhCacheRegionFatory作为缓存实现类,所以需要引起依赖:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-ehcache</artifactId>
        </dependency>
@Data
@ToString
@Entity
@Table(name="people")
// 添加hibernate cache注解
// 开启当前实体的二级缓存
// usage 用于配置对象缓存策略
// 取值来自枚举类CacheConcurrencyStrategy,分别的含义是:
// NONE:没有策略
// READ_ONLY:二级缓存的对象仅允许读取
// NONSTRICT_READ_WRITE:非严格的读写权限
// READ_WRITE:二级缓存的对象可读可写
// TRANSACTIONAL:基于事务的策略
@org.hibernate.annotations.Cache( usage = CacheConcurrencyStrategy.READ_WRITE)
public class People implements Serializable {
    //省略
}

@SpringBootTest
@RunWith(SpringRunner.class)
public class PeopleRepositoryTest {

    @Autowired
    PeopleRepository peopleRepository;

    @Test
    public void testSecondLevelCache(){
        Optional<People> people = peopleRepository.findById(1);
        System.out.println(people.get());


        Optional<People> people1 = peopleRepository.findById(1);
        System.out.println(people1.get());
    }
}

区别于Mybatis是在mapper上添加注解,开启对应的二级缓存,hibernate是在实体类上添加注解,开启对应的二级缓存,并支持配置缓存策略。执行测试方法可以看到,虽然没有使用事务包裹,但是第二次查询并没有实际操作数据库,而是从缓存中读取到对象并返回。

综合上面的分析,可以看到,mybatis和hibernate框架都是默认开启一级缓存,一级缓存主要是对于sqlSession而言的,即一次连接范围内的相同数据库查询操作,会自动存入缓存,后续的操作不需要重复访问数据库,直接从缓存获取即可。一级缓存的作用是提高数据访问效率,其作用范围较小、时间较短(一次session范围内),对应用整体的性能提升有限。二级缓存默认都是没有开启,需要我们手动配置开启它,二级缓存主要是对于sqlSessionFactory而言的,支持多次session间共享,对于同一个表的重复操作,会将涉及的对象实例序列化保存,后续操作不需要重复访问数据库,直接从缓存获取即可。二级缓存一般建议借助缓存介质实现(不然将会占用更多的应用内存),它的作用范围大、时间长(整个应用范围生效,对象实体有更新、删除等变化时,才会清空对应的二级缓存),对应用整体的性能提升显著,尤其是数据库数据量非常大的时候(比如单表千万级数据查询)。

 

***:MyBatis和Hibernate优缺点对比

Mybatis属于一个自定义实现的半自动ORM框架,它不遵循JPA规范,也不支持对象之间的关系映射,因此无法自动建表,也不支持对象导航查询;

Mybatis支持的操作方式主要有三种:1.mapper中实现的单表查询方法;2.编写sql实现自定义方法;3.通过QBE(注意这里的E是mybatis自定义的Example,不是jpa规范中的example类)实现自定义查询;

由于Mybatis支持的操作方式有限,所以对于批量操作没有良好的支持,只能通过手写sql来实现;返回自定义类型方面,mybatis有良好的支持。

Hibernate则是遵循JPA规范的全自动ORM框架,支持对象之间的关系映射,可以自定义数据库表行为,支持对象导航查询;

Hibernate支持的操作方式主要有五种:1.repository自带的方法;2.QBE(E是jpa规范的Example);3.QBC(利用Specification对象实现操作)4.HQL(Hibernate支持的自定义语言);5.SQL;

Hibernate良好的支持批量操作,repository自带的删除方法就有直接支持批量删除的,也可以通过em和开启配置,来便捷的实现批量插入、更新;但是在支持返回自定义类型上,没有mybatis便捷,需要在HQL语句中指定自定义类型的构造器。

在使用方面,由于mybatis是国产的,文档更加亲和国内,且不需要学习jpa、spring data jpa,所以入门门槛较低,使用更加便捷;另外在单表条件查询上,mybatis支持直接传入对象实例,进行条件查询,且自动识别对象的属性作为条件值,hibernate则不支持这么便捷的方法;此外就是mybatis做好了缓存、延迟加载、数据库连接池等封装和对接,我们只需要拿来直接使用即可,无需过多关注,除非想针对性的进行调优。

Hibernate是JPA规范的底层实现,所以使用hibernate之前,首先需要学习jpa,然后由于spring、springboot一统天下,所以还要学习spring data jpa,文档也多是外语,入门门槛较高;在单表条件查询上,没有类似mybatis的直接传入对象实例查询,相对要麻烦一点;在缓存方面,Hibernate默认开启了一级缓存,要想实现更加高级的缓存功能,则需要用户深入Hibernate框架,进行对应的配置开启;在数据库连接池上,hibernate也没有做相应的封装,也需要用户深入框架,进行对应的配置开启。

 

***:Hibernate自定义主键生成策略

前面有讲到hibernate定义好的四种主键生成策略:AUTO、IDENTITY、TABLE、SEQUENCE。如果我们在创建实体类时,没有指定主键生成策略,则hibernate默认采用AUTO策略。而有的时候,我们需要自己设置主键id,而不是由hibernate去自动生成,尤其是分布式系统中这一现象非常常见。这时就需要实现自定义主键生成策略,当我们插入记录的时候设置了id值,则使用我们设置的主键值,没有设置,则由自定义主键生成器进行生成。具体实现如下:

1.直接使用默认的AUTO策略

AUTO策略就是由程序决定主键生成,所以它是支持设置主键值的。

@Entity
@Table(name = "tb_food")
@Data
public class Food implements Serializable {

    @Id
    private String id;
    private String category;
    private String producer;
}

@SpringBootTest
@RunWith(SpringRunner.class)
public class FoodRepositoryTest {

    @Autowired
    private FoodRepository repository;

    @Test
    public void testDefineId(){
        Food food = new Food();
        food.setId("123456");
        food.setCategory("方便面");
        food.setProducer("康师傅");
        repository.save(food);
    }

}

如上,要求每次插入记录的时候必须设置主键id的值,否则将会报错:

org.springframework.orm.jpa.JpaSystemException: ids for this class must be manually assigned before calling save()

2.自定义主键生成策略

很明显虽然方法1很简单,但是缺陷也很大,一旦我们没有设置主键值,就会插入失败。要想既支持设置主键id,又支持未设置时,程序按照定义好的规则自动生成一个,就需要实现自定义主键生成策略:

@Entity
@Table(name = "tb_food")
@Data
public class Food implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO,generator = "custom_id")
    @GenericGenerator(name = "custom_id",strategy = "com.zst.learnhibernate.domain.CustomUUIDGenerator")
    public String id;
    private String category;
    private String producer;
}


/**
 * 自定义主键策略生成器
 * 1.当主键id有值时,使用该值作为主键
 * 2.如果没有值,则由系统UUID随机生成UUID字串默认填充
 */
@Slf4j
public class CustomUUIDGenerator implements IdentifierGenerator {

    @Override
    public Serializable generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException {
        try {
            Object id = FieldUtils.readField(o, "id");
            if (id != null) {
                return (Serializable) id;
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
        return UUID.randomUUID().toString().replaceAll("-","");
    }
}


@SpringBootTest
@RunWith(SpringRunner.class)
public class FoodRepositoryTest {

    @Autowired
    private FoodRepository repository;

    @Test
    public void testDefineId(){
        Food food = new Food();
        //food.setId("123456");
        food.setCategory("方便面");
        food.setProducer("康师傅");
        repository.save(food);
    }
}

如上,自定义CustomUUIDGenerator类实现IdentifierGenerator接口,实现generator方法,通过反射获取实例的id属性值(由于反射获取,注意将该属性修饰符改为public,否则无法获取),如果没有拿到值,则由UUID.randId()随机生成一个。这样配置之后,就是实现了在插入记录时,,当设置了主键id值,使用设置的值,没有设置则由系统UUID随机生成一个。

***:小贴士

hibernate.id.UUIDGenerator建议少用,博主在实际使用过程中发现,存在异常(设置或者不是设置主键值,都是系统随机生成,设置的值无法到达数据库)。此外,注意主键属性的修饰符改为public,否则反射获取属性值报错(当然也可以自己写反射逻辑,暴力反射可以获取所有属性值,即使是private)。


以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值