欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-11 09:11:16
SpringBoot集成Hibernate Jpa、Spring data jpa和Hibernate之间有什么关系?JPA规范之主键策略JPA规范之数据库表行为 单表增删改查 getOne()和findById()的区别?单表条件查询 如何返回自定义类型?对象导航查询 执行查询为什么还要添加@Transaction注解? SQL、QBC、QBE、HQL的概念和优缺点对比 mysql的外键关联策略有哪些?什么是一级缓存,什么是二级缓存?有什么作用? MyBatis和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)

项目结构图:

Java EE--框架篇(3-2)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,具体看下它包含哪些依赖:

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

从上图可以看到,原来这个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.添加方法(兼具更新功能)

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

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

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

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

2.删除方法

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

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.查询方法

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

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():

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

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

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

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

执行testQuery():

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

***:执行查询为什么还要添加@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,操作数据库;后面的查询都是先从缓存中获取。

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

    @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():

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

testCache():

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

可以看到虽然两个方法的代码完全一样,但是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接口,支持序列化。

Java EE--框架篇(3-2)Hibernate
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);
    }
}

执行截图如下:

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

可以看到使用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也没有做相应的封装,也需要用户深入框架,进行对应的配置开启。

 


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

本文地址:https://blog.csdn.net/i18n486/article/details/110184898