文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Hibernate对象管理入门?看这一篇就够了

2024-11-29 20:07

关注

实现原理

ORM(对象关系映射)
Session管理
查询语言
缓存机制

状态维护

Hibernate通过Session对象维护实体对象的生命周期状态。主要有以下几个状态:

Transient(瞬态)
Persistent(持久化)
Detached(游离)
Removed(删除)

Hibernate状态维护的具体实现

Transient状态
Persistent状态
Detached状态
Removed状态

事务管理和缓存机制

事务管理
一级缓存
二级缓存

示例代码

// Transient状态
Person person = new Person();
person.setName("John");
// Persistent状态
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(person); // person对象现在是Persistent状态
tx.commit();
session.close(); // person对象现在是Detached状态
// Removed状态
Session newSession = sessionFactory.openSession();
Transaction newTx = newSession.beginTransaction();
newSession.delete(person); // person对象现在是Removed状态
newTx.commit();
newSession.close();

通过这种机制,Hibernate能够高效地管理对象与数据库记录之间的映射,并自动处理数据的持久化和同步工作。

Hibernate的示例代码

例子构建

我们有一个配置管理的功能,现在定义了一个系统配置的表:biz_conf

建表语句

-- auto-generated definition
drop sequence if exists seq_seed;
create sequence seq_seed
    maxvalue 65535
    cycle;


alter sequence seq_seed owner to postgres;


drop function if exists snowflake_id;
create function snowflake_id(seed integer) returns bigint
    language plpgsql
as
$$
DECLARE
    start_epoch INT8 := 1546300800000;
    seq_id      INT8 := 0;
    now_millis  INT8 := 0;
    unique_id   INT8 := 0;
    region_val  int  := seed % 1024;
BEGIN
    SELECT nextval('seq_seed') % 4095 INTO seq_id;
    SELECT floor(extract(EPOCH FROM clock_timestamp()) * 1000) INTO now_millis;
    unique_id := (now_millis - start_epoch) << 22;
    unique_id := unique_id | (region_val << 10);
    unique_id := unique_id | seq_id;
    RETURN unique_id;
END;
$$;


alter function snowflake_id(integer) owner to postgres;


drop function if exists random_id;
create function random_id() returns bigint
    language plpgsql
as
$$
BEGIN
    RETURN snowflake_id(0);
END;
$$;


alter function random_id() owner to postgres;


drop table if exists biz_conf;
create table biz_conf
(
    id           bigint       default random_id()       not null
        constraint biz_conf_pkey
            primary key,
    key_id       varchar(255)                               not null,
    val          varchar(255) default ''::character varying not null,
    created_time timestamp    default now()                 not null
);


-- 添加新增的几个配置 sftp的
INSERT INTO biz_conf (key_id, val) VALUES
    ('sftp服务器地址', ''),
    ('sftp服务器端口', '22'),
    ('sftp服务器账号', ''),
    ('sftp服务器密码', ''),
    ('sftp上传路径', './'),
    ('sm2公钥',''),
    ('sm2私钥','');

关系实体映射

我们在目录entity下面存放关系实体映射

import jakarta.persistence.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;


import java.sql.Timestamp;




@Entity
@Table(name = "biz_conf")
@DynamicInsert
@DynamicUpdate
public class BizConf {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, unique = true, columnDefinition = "INT8")
    public Long id;


    @Column(name = "key_id", length = 256)
    public String key_id;


    @Column(name = "val",length = 256)
    public String val;


    @Column(name = "created_time", updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    public Timestamp created_time;
}

经过我们的实践中,在关系实体映射中,可以将字段直接置为public,同时将字段名直接与数据库字段名保持一致。便于后续映射。(这里可以采用private+驼峰的模式,再使用Lombok,但是代码直观性不够,如果没有接触过lombok,将几乎看不出来原理,同时我们对各个反序列化框架对lombok的支持度存有一定的担心,故这里我们是建议使用public + 原始字段名)。

构建领域对象实体

import com.cqcdi.siem.log_audit_admin.entity.biz.BizConf;
import com.cqcdi.siem.log_audit_admin.maindao.BaseEntityDomain;



public class BizConfModel  extends BaseEntityDomain {


    public BizConfModel() {}


    public BizConfModel(Long id){
        super(id);
    }


    @Override
    protected Class getEntityClass() {
        return BizConf.class;
    }


    @Override
    protected String getEntityClassName() {
        return "BizConf";
    }
}

领域对象实体构建完毕之后,我们就可以直接使用。这里的领域实体与我们的dao是有区别的,dao层一般是一个spring bean,也就是说基本上是全局单例,不会有对象状态。但是我们可以发现,领域实体实际上是没有被标记未一个bean的,所以领域实体实际上是可以管理对象的状态的。

正因为如此,hibernate和领域对象的概念结合,我们可以把对象的各种状态管理起来。例如,我们可以先查出一个对象,修改了一些值之后,直接调用刷新机制。(这个与瞬时态的写法其实是类似的,但是理念不太一样)

例子

使用领域模型查询和更新


    @Test
    public void demo1() {
        BizConfModel bizConfModel = new BizConfModel();
        List bizConfs = bizConfModel.getAll();
        bizConfs.stream().forEach(bizConf -> {
            System.out.println(bizConf.key_id + " " + bizConf.val);
            bizConf.val = "add_"+ DatetimeUtil.getViewStrOfDatetime(LocalDateTime.now()) + "_"+ bizConf.val; // 对象处于被管理的状态,如果修改了对象,直接调用flush即可入库
            bizConfModel.flush();
        });
    }

使用瞬时态查询和更新(更新不会生效)


    @Test
    public void demo2(){
        List bizConfs = BaseEntityUtils.getAll(BizConf.class);
        bizConfs.stream().forEach(bizConf -> {
            System.out.println(bizConf.key_id + " " + bizConf.val);
            bizConf.val = "add_"+ DatetimeUtil.getViewStrOfDatetime(LocalDateTime.now()) + "_" + bizConf.val; // 对象处于瞬时态态,如果修改了对象,直接调用flush,没有入库
            BaseEntityUtils.flush(); // 不生效
        });
    }

使用领域模型、瞬时工具类、原生sql和jpql进行分页查询


    @Test
    public void demo3(){
        // 使用领域模型进行分页查询
        BizConfModel domain = new BizConfModel();
        domain.setDomainEntity(new BizConf());
        domain.getDomainEntity().key_id = "sm2私钥";
        Page ans = domain.defaultPage(1,10);


        ans.getContent().stream().forEach(bizConf1 -> {
            System.out.println(bizConf1.key_id + " " + bizConf1.val);
        });
        System.out.println(" ------------------ ");


        // 使用工具类进行分页查询
        BizConf bizConf = new BizConf();
        bizConf.key_id = "sm2私钥";
        Page bizConfs2 = BaseEntityUtils.defaultPage(bizConf,1,10,BizConf.class);
        bizConfs2.getContent().stream().forEach(bizConf1 -> {
            System.out.println(bizConf1.key_id + " " + bizConf1.val);
        });
        System.out.println(" ------------------ ");


        // 使用sql分页查询
        // 使用原生sql查询
        List bizConfs1 = BaseEntityUtils.executeNativeQuery("select * from biz_conf offset 1 limit 10",BizConf.class);
        bizConfs1.stream().forEach(bizConf1 -> {
            System.out.println(bizConf1.key_id + " " + bizConf1.val);
        });
        // 使用Jpql查询:把原生sql中的表名替换成对象
        System.out.println(" ------------------ ");
        List bizConfList = BaseEntityUtils.findByConditions("from BizConf",new HashMap<>(),1,10,BizConf.class);
        bizConfList.stream().forEach(bizConf1 -> {
            System.out.println(bizConf1.key_id + " " + bizConf1.val);
        });
    }

Hibernate中的级联操作

Hibernate提供了哪些方法来实现对象关系映射(ORM)

@OneToMany

@OneToMany 注解用于描述一对多(One-to-Many)的关联关系,即一个实体对象关联多个相关实体对象。这种关系通常在数据库中以外键来表示。以下是 @OneToMany 注解的基本使用方式

在一的一方(One)
@Entity
public class ParentEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @OneToMany(mappedBy = "parentEntity")
    private List children;
    // getters and setters
}
在多的一方(Many)
@Entity
public class ChildEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @ManyToOne
    @JoinColumn(name = "parent_entity_id")
    private ParentEntity parentEntity;
    // getters and setters
}

在上述示例中,ParentEntity 拥有多个 ChildEntity,因此在 ParentEntity 类上使用了 @OneToMany 注解,并通过 mappedBy 属性指定了关联关系的属性名,这里是 "parentEntity"。在 ChildEntity 类中,使用 @ManyToOne 注解指定了多的一方的类型,并通过 @JoinColumn 注解指定了关联的数据库表列。 请注意,使用 @OneToMany 注解时,通常需要考虑懒加载、级联操作、集合类型等配置。例如,可以使用 fetch 属性来指定加载策略,使用 cascade 属性来指定级联操作的行为。配置的详细信息会根据具体的业务需求而变化。

@OneToOne

```java
@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @OneToOne(mappedBy = "employee")
    private Address address;
    // getters and setters
}
@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @OneToOne
    @JoinColumn(name = "employee_id")
    private Employee employee;
    // getters and setters
}
```

@ManyToOne

```java
@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
    // getters and setters
}
@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @OneToMany(mappedBy = "customer")
    private List orders;
    // getters and setters
}
```

@ManyToMany

```java
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List courses;
    // getters and setters
}
@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    // other fields
    @ManyToMany(mappedBy = "courses")
    private List students;
    // getters and setters
}
```

这些注解提供了丰富的选项,以满足不同场景下的实体关系映射需求。在使用这些注解时,根据具体的业务需求和数据库表结构,适当地配置关联关系、关联表等信息,以实现正确而高效的数据映射。

级联操作的例子

在Hibernate中,级联操作可以通过在实体类的映射文件或注解中设置相应的级联关系来实现。以下是级联操作的例子:

级联查询

假设有两个实体类:ParentEntity 和 ChildEntity,它们之间存在一对多的关系。可以通过配置级联关系来在查询时同时获取关联的子实体。

@Entity
public class ParentEntity {
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List children;


    // other fields and methods
}


@Entity
public class ChildEntity {
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private ParentEntity parent;


    // other fields and methods
}

在上述例子中,cascade = CascadeType.ALL 表示在父实体上的所有操作都将级联到子实体上,包括查询。

级联插入

ParentEntity parent = new ParentEntity();
ChildEntity child1 = new ChildEntity();
ChildEntity child2 = new ChildEntity();


// 设置关联关系
child1.setParent(parent);
child2.setParent(parent);


// 将子实体添加到父实体的集合中
parent.getChildren().add(child1);
parent.getChildren().add(child2);


// 保存父实体,级联保存子实体
entityManager.persist(parent);

在上述例子中,当保存父实体时,由于设置了级联保存 (cascade = CascadeType.ALL),子实体也会被保存。

级联更新

ParentEntity parent = entityManager.find(ParentEntity.class, parentId);
parent.setName("Updated Parent");


// 子实体的相关属性也被更新
parent.getChildren().forEach(child -> child.setDescription("Updated Child"));


// 保存父实体,级联更新子实体
entityManager.merge(parent);

在上述例子中,由于设置了级联更新 (cascade = CascadeType.ALL),当更新父实体时,子实体的相关属性也会被更新。

级联删除

ParentEntity parent = entityManager.find(ParentEntity.class, parentId);


// 删除父实体,级联删除子实体
entityManager.remove(parent);

在上述例子中,由于设置了级联删除 (cascade = CascadeType.ALL),当删除父实体时,子实体也会被级联删除。

请注意,级联操作应该谨慎使用,确保了解它们的影响,以避免意外的数据变更。

级联操作中的cascade

cascade 是 Hibernate 中用于配置级联操作的重要属性之一。该属性用于指定在对父实体进行特定操作时,是否要同时应用相同的操作到关联的子实体上。cascade 属性通常在关联关系的注解或映射文件中进行配置。 以下是一些常用的 cascade 属性值

CascadeType.PERSIST (级联保存)

当对父实体调用 persist() 方法时,子实体也会被保存。

CascadeType.MERGE (级联更新)

当对父实体调用 merge() 方法时,子实体也会被更新。

CascadeType.REMOVE (级联删除)

当对父实体调用 remove() 方法时,子实体也会被删除。

CascadeType.REFRESH (级联刷新)

当对父实体调用 refresh() 方法时,子实体也会被刷新。

CascadeType.DETACH (级联分离)

当对父实体调用 detach() 方法时,子实体也会被分离。

CascadeType.ALL (全部)

包含所有的级联操作,即包括上述的 PERSIST、MERGE、REMOVE、REFRESH、DETACH。 在实体类的映射文件或使用注解进行配置时,cascade 属性可以直接指定一个或多个级联操作,如下所示

@Entity
public class ParentEntity {
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List children;
    // other fields and methods
}

在上述例子中,cascade = CascadeType.ALL 表示对 ParentEntity 上的所有操作都会级联到关联的 ChildEntity 上。我们可以根据实际需求选择适当的级联操作。然而,要注意过度使用级联操作可能导致不必要的性能开销和意外的数据变更。

级联操作的好处

级联更新(CascadeType.MERGE)和级联插入(CascadeType.PERSIST)在Hibernate中确实带来了一些好处,但同时也需要谨慎使用以避免潜在的问题。以下是它们的一些优势

级联更新(CascadeType.MERGE)的好处

简化操作

级联更新允许我们在对父实体进行 merge() 操作时,同时更新关联的子实体。这样,我们不需要手动处理每个关联实体的更新。

维护关联关系

级联更新确保了关联实体与父实体的状态同步,无需手动调用 merge() 方法来更新关联实体。

减少代码冗余

避免了手动维护关联实体状态的繁琐操作,简化了代码逻辑。

级联插入(CascadeType.PERSIST)的好处

原子性

当我们持久化一个新的父实体时,级联插入可以确保相关的子实体也被保存。这确保了相关实体的一致性,不需要单独保存每个关联实体。

简化操作

通过级联插入,我们不需要为每个关联实体显式调用 persist() 方法。Hibernate会自动保存相关的关联实体。

减少错误

避免了遗漏保存关联实体的错误,提高了代码的可维护性。 然而,尽管级联更新和插入带来了便利,但需要注意以下几点

性能开销

过度使用级联操作可能导致性能开销,尤其是在涉及大量关联实体的情况下。谨慎选择使用级联,以避免不必要的数据库操作。

数据一致性

级联操作可能导致数据一致性的问题。例如,级联插入可能会导致关联实体被保存,即使它们不是父实体的直接子元素。

潜在的循环

避免配置导致循环级联操作,可能导致无限递归或栈溢出的问题。 总体而言,级联操作是强大的工具,但在使用时需要仔细考虑,并根据具体的业务需求和性能考虑来选择适当的级联策略。

使用级联操作的策略

使用级联操作时,需要考虑不同的策略、场景和注意事项,以确保数据的一致性、性能和可维护性。以下是一些常见的策略、使用场景和注意事项

策略

CascadeType.MERGE(级联更新)

适用场景

当我们希望在更新父实体时同时更新关联的子实体时。

注意事项

避免循环级联,确保不会形成无限递归的级联更新。

CascadeType.PERSIST(级联插入)

适用场景

当我们希望在插入父实体时同时插入关联的子实体时。

注意事项

小心处理关联实体之间的引用,以避免不必要的插入操作。

CascadeType.REMOVE(级联删除)

适用场景

当我们希望在删除父实体时同时删除关联的子实体时。

注意事项

谨慎使用级联删除,确保了解删除操作的影响,避免误删关联实体。

CascadeType.ALL(全部)

适用场景

当我们希望对父实体的所有操作都级联到关联的子实体时。

注意事项

尽量避免使用过多的级联操作,只选择实际需要的操作,以提高性能和可控性。

使用场景

一对一关系

当两个实体之间是一对一关系时,可以使用级联来确保它们的状态同步。

一对多关系

在一对多关系中,级联操作可用于确保在更新或删除父实体时,相关的子实体也会被同步更新或删除。

父子关系

在父子关系中,级联插入和更新可简化操作,确保整个对象图的一致性。

注意事项

性能开销

过度使用级联操作可能导致性能问题。在选择级联操作时,需要考虑数据库的负担和查询效率。

循环级联

避免配置导致循环级联操作,可能导致无限递归或栈溢出的问题。

数据一致性

注意级联操作可能导致的数据一致性问题,特别是在删除操作中。

关联实体引用

确保处理关联实体之间的引用,以避免不必要的插入和更新操作。

深度级联

谨慎使用深度级联,尽量避免将级联操作应用到整个对象图。

总体而言,级联操作是一个强大的特性,但需要谨慎使用。根据具体的业务需求和性能要求,选择适当的级联策略,并确保了解每个级联操作的影响。测试和审查数据变更的结果也是使用级联操作时的关键步骤。

避免循环级联的方法

循环级联

什么是循环级联

循环级联(Cascading Cycle)是指在对象关系映射(ORM)中,特别是在Hibernate等ORM框架中,通过级联操作导致的循环引用或无限递归的情况。 在ORM中,级联操作是指在对一个实体进行操作时,自动对其关联的其他实体进行相应的操作,比如保存、更新或删除。循环级联则是指这种级联操作形成了一个循环链,其中实体之间的级联操作形成一个循环,可能导致无限递归或栈溢出等问题。 例如,考虑两个实体Parent和Child,它们之间是双向关联的

// Parent Entity
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List children;
// Child Entity
@ManyToOne
private Parent parent;

在上述例子中,当我们尝试保存或更新一个Parent实体时,由于配置了CascadeType.ALL,将触发对关联的Child实体进行级联操作。如果在Child实体中也配置了类似的级联操作,就可能形成一个循环链。 循环级联可能导致以下问题

无限递归

级联操作不断触发,导致无限递归,最终可能导致栈溢出或应用程序无法正常执行。

性能问题

无限递归会导致性能下降,因为系统不断地执行相同的级联操作。 为了避免循环级联,需要谨慎配置级联操作,避免形成循环链,或者使用一些技术手段(如懒加载、手动维护关联关系等)来规避潜在的问题。在使用ORM框架时,了解和审查关联关系、级联配置以及避免双向关联等是很重要的。

循环级联的例子

以下是一个简单的循环级联的例子,使用Java中的Hibernate注解。考虑两个实体类Person和Address,它们之间是双向关联的,并且都配置了级联保存操作。

import javax.persistence.*;
@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id")
    private Address address;
    // Getter and Setter methods
}
@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String street;
    @OneToMany(mappedBy = "address", cascade = CascadeType.ALL)
    private List residents;
    // Getter and Setter methods
}

在上述例子中,Person和Address之间存在双向关联。一个Person实体拥有对应的Address实体,而一个Address实体拥有对应的List。此外,配置了级联操作,使得对Person实体的保存操作会级联保存关联的Address实体,而对Address实体的保存操作也会级联保存关联的Person实体。 这种配置可能导致循环级联的问题。例如,当我们尝试保存一个Person实体时,Hibernate会自动保存关联的Address实体,而在保存Address实体时,又会级联保存关联的Person实体,形成了一个循环链。 为了避免循环级联,可以通过适当配置级联操作、使用mappedBy来指定关系的拥有方、懒加载等方式来规避潜在的问题。在实际应用中,需要根据具体场景来合理设计实体之间的关系以及级联操作。

如何检测实体级联中是否有级联循环

检测实体级联中是否存在级联循环是一项重要的任务,因为级联循环可能导致无限递归或栈溢出等问题。以下是一些方法,我们可以使用它们来检测实体级联中是否存在潜在的级联循环

使用工具或插件:

有一些工具和插件可以帮助检测级联循环。例如,Hibernate Envers(Hibernate的审计插件)可以用于检测潜在的级联问题。在使用这些工具时,我们需要按照它们的文档进行配置和使用。 也可以生成对象图或者类图进行分析。

手动分析映射关系:

仔细分析实体之间的映射关系,特别是双向关联的情况。确保在双向关联时,通过使用mappedBy等属性正确指定关系的拥有方,以避免循环级联。

测试场景:

创建一些简单的测试场景,执行级联操作,观察是否会发生无限递归或栈溢出。这种方法虽然相对简单,但可能不适用于复杂的数据模型。

审查日志:

在Hibernate的日志中启用详细的日志记录级别,观察是否有关于级联操作的异常或警告。有时,日志中会提供有关级联操作的有用信息,以便我们能够识别潜在的循环问题。

使用断点调试:

使用调试工具在执行级联操作时设置断点。观察在实体关系中是否存在循环,以及在何处可能触发无限递归。

自定义检测逻辑:

编写自定义的检测逻辑,检查实体之间的关联关系。这可能涉及递归地检查实体的关联关系,并确保没有循环。这种方法可能相对复杂,但可以提供更精细的控制。 我们应该注意到的是,由于每个应用程序和数据模型都是独特的,没有一种通用的方法适用于所有情况。建议使用上述方法的组合,根据我们的具体场景选择最适合的方式。在检测到潜在的级联循环问题时,及时调整映射配置,以避免可能的运行时异常。

解决方案

在Hibernate中,实体级联操作可能导致循环级联操作,为了避免这种问题,我们可以采取以下一些方案

使用CascadeType中的subset进行限制:

在@OneToMany或@ManyToMany注解中,通过cascade属性指定级联操作的类型,可以选择性地配置级联,而不是使用CascadeType.ALL。使用CascadeType.PERSIST、CascadeType.MERGE等来限制级联的范围,以避免不必要的级联操作。

@OneToMany(mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set children;

手动维护关联关系:

不使用级联,手动管理关联关系。在进行操作时,手动添加或删除关联关系,而不依赖于级联操作。这样可以更精确地控制实体之间的关系。

使用orphanRemoval属性:

当orphanRemoval设置为true时,如果从父实体中移除了某个子实体,该子实体将被删除。这可以避免在删除父实体时引起子实体的级联删除,从而防止循环级联。

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List children;

懒加载:

将关联关系设置为懒加载,这样只有在实际使用关联实体时才会加载。通过延迟加载,可以减少不必要的级联操作。

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List children;

避免双向关联:

双向关联时,容易出现循环级联问题。考虑是否真的需要双向关联,如果可以单向关联就单向关联,从而避免循环级联的发生。

// 避免双向关联
// ParentEntity
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List children;
// ChildEntity
@ManyToOne
private ParentEntity parent;

使用@JsonManagedReference和@JsonBackReference (仅在使用Jackson等库进行JSON序列化时有效):

如果我们使用JSON序列化工具,如Jackson,我们可以使用@JsonManagedReference和@JsonBackReference来解决循环引用问题。

// ParentEntity
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
@JsonManagedReference
private List children;
// ChildEntity
@ManyToOne
@JsonBackReference
private ParentEntity parent;

选择适合我们场景的方案,根据需求进行灵活配置,以避免循环级联操作带来的问题。

Hibernate中关键类的实现原理

SessionFactory

SessionFactory 是 Hibernate 中的一个关键接口,它负责创建 Session 对象。SessionFactory 的实现原理涉及到 Hibernate 的启动、配置、元数据加载、缓存管理等方面。 以下是 SessionFactory 的主要实现原理

Hibernate的启动过程

当应用程序启动时,Hibernate 会初始化并构建 SessionFactory。这通常发生在应用程序启动时的某个初始化阶段。SessionFactory 的创建是一个开销较大的操作,因为它需要加载元数据、配置信息以及构建内部数据结构。

配置信息加载

Hibernate 通过读取配置文件(如 hibernate.cfg.xml)或者通过编程方式配置,获取数据库连接信息、Hibernate属性、映射文件的位置等配置信息。这些配置信息被用于创建数据库连接池、指定方言、配置缓存等。

元数据加载

Hibernate需要加载应用程序中的实体类和映射文件,以建立 Java 对象与数据库表之间的映射关系。元数据加载的过程包括解析注解或 XML 映射文件,构建实体类的元数据,然后将这些元数据注册到 SessionFactory 中。

缓存管理

SessionFactory 负责管理 Hibernate 的缓存机制,包括一级缓存(Session 级别的缓存)和二级缓存(SessionFactory 级别的缓存)。这些缓存用于提高数据访问性能,减少对数据库的频繁访问。

SessionFactory 在 Hibernate 中负责管理缓存,主要包括一级缓存(Session 级别的缓存)和二级缓存(SessionFactory 级别的缓存)。缓存的使用旨在提高性能,减少对数据库的频繁访问。

一级缓存(Session 级别的缓存)

一级缓存介绍
缓存的生命周期
缓存的管理

二级缓存(SessionFactory 级别的缓存)

二级缓存介绍
配置二级缓存
true
org.hibernate.cache.ehcache.EhCacheRegionFactory
支持的缓存提供者
使用缓存
缓存策略

线程安全性

SessionFactory 是线程安全的,多个线程可以共享一个 SessionFactory 实例。SessionFactory 通过多个线程共享可以提高性能,因为它们可以共享一些元数据和缓存,减少资源开销。

SessionFactory 在 Hibernate 中是线程安全的。它的线程安全性是通过以下几个机制来保证的

不可变性

在 Hibernate 中,SessionFactory 是一个不可变的对象。一旦创建,其状态不可修改。这意味着在多线程环境中,多个线程可以同时访问 SessionFactory 实例,而不会导致数据不一致或竞态条件。

线程本地实例

Hibernate 通过将 SessionFactory 与线程进行绑定,确保每个线程都拥有独立的 Session 实例。这是通过使用 ThreadLocal 来实现的。当应用程序中的一个线程需要创建或获取 Session 时,SessionFactory 会为该线程创建一个独立的 Session 实例,并将其与线程关联起来。这样,每个线程都有自己的 Session 实例,而不会互相干扰。

public class HibernateUtil {
    private static final SessionFactory sessionFactory;
    static {
        sessionFactory = new Configuration().configure().buildSessionFactory();
    }
    private static final ThreadLocal threadLocalSession = new ThreadLocal<>();
    public static Session getSession() {
        Session session = threadLocalSession.get();
        if (session == null) {
            session = sessionFactory.openSession();
            threadLocalSession.set(session);
        }
        return session;
    }
}

上述示例中,HibernateUtil 类的 getSession 方法用于获取与当前线程关联的 Session 实例。

单例模式和懒加载

通常情况下,SessionFactory 是以单例模式创建的,确保在整个应用程序生命周期内只有一个 SessionFactory 实例。这减少了资源开销并提高了性能。此外,SessionFactory 的创建通常是在应用程序启动时进行的,而不是在每次需要 Session 时创建。这种懒加载的策略也有助于提高性能。 通过这些机制,Hibernate 的 SessionFactory 在多线程环境中可以安全地被多个线程共享,而不会引起竞争条件或线程安全问题。注意,尽管 SessionFactory 是线程安全的,但 Session 对象本身并不是线程安全的,因此在多线程环境中使用 Session 时需要谨慎处理。

创建 Session

SessionFactory 的主要责任之一是创建 Session 实例。Session 是 Hibernate 中用于进行数据库操作的主要接口。通过 SessionFactory 创建的 Session 具有与数据库的物理连接,并负责管理事务、执行查询、保存、更新和删除操作等。 总体而言,SessionFactory 的实现原理涉及到配置加载、元数据加载、缓存管理等多个方面,其设计旨在提高性能、简化开发过程,并提供对数据库操作的封装。 Hibernate 的 SessionFactory 通常是一个长期存在的对象,负责整个应用程序的数据库访问。

SessionFactory提供了哪些方法

SessionFactory 接口是 Hibernate 中用于创建 Session 实例的工厂,它定义了一系列用于创建和配置 Session 的方法。以下是 SessionFactory 接口的一些主要方法

openSession()

Session openSession()

该方法用于创建一个新的 Session 实例。每个 Session 实例都表示一个与数据库的物理连接。

openStatelessSession()

StatelessSession openStatelessSession()

该方法用于创建一个新的 StatelessSession 实例。与普通的 Session 不同,StatelessSession 不维护持久化上下文,对应用程序更加透明。

withOptions()

Session openSession(Interceptor interceptor)

该方法用于创建一个新的 Session 实例,并允许通过 Interceptor 对象来自定义对数据库操作的拦截。Interceptor 接口允许我们在执行 SQL 语句之前和之后执行一些额外的逻辑。

getCurrentSession()

Session getCurrentSession() throws HibernateException

该方法用于获取当前线程关联的 Session 实例。通常与 Hibernate 的事务管理机制一起使用。需要在配置文件中启用事务管理,并在事务管理的上下文中才能使用。

openTemporarySession()

Session openTemporarySession()

该方法用于创建一个新的临时 Session 实例。临时 Session 在处理临时数据时可能会更加高效,但通常情况下,使用 openSession() 是更常见的选择。

close()

void close()

该方法用于关闭 SessionFactory,释放资源。在应用程序关闭时调用,或者在不再需要 SessionFactory 时调用。通常由应用程序的生命周期管理来调用。 这些方法代表了 SessionFactory 提供的主要功能,其中最常用的是 openSession() 用于创建 Session 实例。在使用 SessionFactory 时,还需要注意线程安全性和资源管理,以便正确地管理 Session 的生命周期。

EntityManager

在 Hibernate 中,EntityManager 对象是与持久性上下文(Persistence Context)交互的主要接口。Hibernate 提供了两种主要的持久性上下文管理方式,一种是使用 JPA 规范定义的 EntityManager,另一种是使用 Hibernate 提供的 Session。EntityManager 主要用于以下作用

实体管理

EntityManager 负责实体的生命周期管理,包括实体的持久化、合并、移除以及查询等操作。

事务管理

在 JPA 中,EntityManager 通常与事务一起使用。它可以参与容器管理的事务,或者通过 EntityTransaction 来手动管理事务。

查询

EntityManager 提供了强大的查询功能,支持 JPQL(Java Persistence Query Language)和 Criteria 查询。通过这些查询方式,可以方便地检索实体对象。

缓存管理

EntityManager 负责缓存实体对象,以提高性能。缓存可以在一定程度上避免数据库频繁的读写操作。

在 JPA(Java Persistence API)中,EntityManager 接口提供了一些用于管理缓存的方法。这些方法主要用于实体对象的加载、存储以及刷新等操作。以下是一些与缓存管理相关的 EntityManager 方法

find(Class entityClass, Object primaryKey)
MyEntity entity = entityManager.find(MyEntity.class, primaryKey);
getReference(Class entityClass, Object primaryKey)
MyEntity entity = entityManager.getReference(MyEntity.class, primaryKey);
persist(Object entity)
entityManager.persist(newEntity);
merge(T entity)
MyEntity managedEntity = entityManager.merge(detachedEntity);
refresh(Object entity)
entityManager.refresh(entity);

这些方法都涉及到一级缓存的管理,一级缓存是持久性上下文中的缓存,用于存储已加载的实体对象。这些方法的使用可以有效地管理缓存,提高应用程序的性能。

对象状态变更

EntityManager 监听实体对象的状态变更,并在适当的时候同步这些变更到数据库。这包括脏检查(dirty checking)和自动更新。 在 Hibernate 中,如果我们使用 JPA 规范,可以通过 EntityManagerFactory 创建 EntityManager 实例。如果我们使用 Hibernate 的本地 API,可以使用 SessionFactory 创建 Session。这两者在实际使用中有一些细微的区别,但本质上都是用来管理持久化操作和对象状态的工具。

在 Hibernate 中,对象状态变更的实现主要依赖于以下两个机制

脏检查(Dirty Checking)

Hibernate 使用脏检查来跟踪对象状态的变化。当一个对象从数据库加载到内存时,Hibernate 会在内存中保留一个快照(snapshot)以记录对象在加载时的状态。在事务提交之前,Hibernate 会再次检查对象的当前状态与快照的状态是否有变化,如果有变化,则认为对象是“脏的”(dirty)。Hibernate 将检测到的变化同步到数据库中,以确保与数据库的数据一致性。

Write-Behind机制

Hibernate 使用 Write-Behind 机制来最小化数据库写操作的次数。在事务提交时,Hibernate 不会立即将所有的变更写入数据库,而是将变更先记录到事务的缓存中。在事务提交完成后,Hibernate 将所有的变更一次性地批量写入数据库,以减少与数据库的通信次数。

Write-Behind 机制是一种数据库写入优化技术,用于减少对数据库的频繁写操作,提高性能。这一机制的核心思想是延迟(或批量)写入数据库,而不是在每次数据变更时立即写入。 在 Hibernate 中,Write-Behind 机制通常与缓存一起使用,以最小化数据库写操作的次数。当一个事务中的对象发生变更时,变更不会立即写入数据库,而是先被记录到缓存中。这样,在事务提交时,Hibernate 可以收集所有的变更,然后一次性地批量写入数据库。 Write-Behind 机制带来了以下几个优势

减少数据库写操作

通过延迟写入,可以减少对数据库的频繁写操作,提高性能。

批量写入

将多个变更一次性批量写入数据库,减少了与数据库的通信次数,进而提高了吞吐量。

异步写入

在一些情况下,写入操作可以异步进行,不会阻塞当前事务的执行。这有助于提高事务的响应性。

缓解锁冲突

如果多个事务并发修改同一数据,Write-Behind 机制可以减少对数据库的锁定需求,从而降低锁冲突的可能性。 要启用 Write-Behind 机制,通常需要配置相关的参数和策略。Hibernate 提供了一些配置选项,以便开发人员可以根据应用程序的需求进行调整。在使用 Hibernate 缓存和进行大规模数据操作时,考虑使用 Write-Behind 机制是一种常见的优化手段。

这两个机制的结合使得 Hibernate 能够高效地管理对象状态的变更。当我们修改了一个持久化对象的属性时,Hibernate会通过脏检查来检测对象的状态是否发生变化。如果变化,Hibernate 将更新缓存中的对象状态,并在事务提交时使用 Write-Behind 机制将变更同步到数据库。 这种机制不仅减少了数据库的访问次数,还提高了性能。但需要注意的是,对于大规模的数据操作,仍然需要慎重使用,以免引起性能问题。在一些情况下,我们可能需要手动调整 Hibernate 的配置,或者使用特定的注解和 API 来优化数据操作的性能。

Session

在Hibernate中,Session 类是与数据库交互的主要接口之一,它代表了一个与数据库的一次会话。Session 提供了一系列的方法来执行数据库操作,包括保存、更新、删除、查询等。以下是Session 类的主要作用和一些常用的 API

主要作用

管理持久化对象

Session 负责管理持久化对象的生命周期,包括从数据库中检索对象、将对象持久化到数据库、更新对象状态以及删除对象。

执行数据库操作

通过 Session,可以执行各种数据库操作,如保存、更新、删除和查询。这使得开发者能够直接与数据库交互而无需编写原始的 SQL 语句。

实现缓存

Session 包含一个缓存,用于存储已加载的对象,以提高性能。这样,当对同一实体进行多次查询时,Hibernate 可以避免重复的数据库查询,从而提升效率。

事务管理

Session 提供了事务管理的功能,可以通过 beginTransaction()、commit() 和 rollback() 方法来管理事务,确保数据库操作的一致性。

常用的 API

save(Object obj)

保存一个对象到数据库。如果对象已经存在,会抛出异常。

update(Object obj)

更新一个对象到数据库。如果对象不存在,会抛出异常。

saveOrUpdate(Object obj)

如果对象不存在,则保存它;如果对象已存在,则更新它。

delete(Object obj)

从数据库中删除一个对象。

get(Class clazz, Serializable id)

通过主键获取对象。

load(Class clazz, Serializable id)

通过主键加载对象。与 get 类似,但 load 返回一个代理对象,只有在访问其属性时才会真正加载。

createQuery(String hql)

创建一个 HQL(Hibernate Query Language)查询对象。

createSQLQuery(String sql)

创建一个原生 SQL 查询对象。

beginTransaction()

开启事务。

commit()

提交事务。

rollback()

回滚事务。

clear()

清空缓存中的所有对象。

flush()

强制将缓存中的数据同步到数据库。

这些方法提供了对数据库的常用操作,通过使用Session,开发者可以更方便地进行数据库交互而无需处理底层的数据库连接和SQL语句。需要注意的是,每个Session实例通常对应一个数据库连接,因此在使用完毕后应该及时关闭,以释放数据库资源。

EntityManager和Session对比

EntityManager 和 Session 是分别来自 JPA(Java Persistence API)和 Hibernate 的两个重要接口,用于进行持久化操作。以下是它们之间的一些对比:

来源和标准

实现方

包含的功能

提供了 JPA 标准的 CRUD 操作。

具有查询语言 JPQL(Java Persistence Query Language)。

支持事务和事务控制。

支持持久化上下文和一级缓存。

提供了丰富的 Hibernate 特有的功能,如 Criteria 查询、HQL(Hibernate Query Language)查询等。

具有事务和事务控制。

提供了二级缓存(SessionFactory 级别的缓存)。

生命周期

查询语言

关联

适用范围

总的来说,选择使用 EntityManager 还是 Session 取决于我们的项目使用的持久化框架和标准。如果我们在使用 JPA,通常使用 EntityManager;如果我们在使用 Hibernate,可以使用 Session。在某些情况下,两者可以混合使用,但要确保它们不会相互冲突。

来源:海燕技术栈内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯