Java EE--框架篇(3-2)Hibernate
目录
快速入门Hibernate(SpringBoot集成Hibernate)
***:没有任何Hibernate出现,为什么叫集成Hibernate?
***:Jpa、Spring data jpa和Hibernate之间有什么关系?
***:一对多,对象导航保存时,可以仅仅调用“多”的一方的保存方法吗?
***:执行查询为什么还要添加@Transaction注解,开启事务?
***:mysql的外键关联策略有哪些?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 的主要实现有Hibernate、EclipseLink 和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接口,支持序列化。
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也没有做相应的封装,也需要用户深入框架,进行对应的配置开启。
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!
本文地址:https://blog.csdn.net/i18n486/article/details/110184898