学习笔记: JPA 与 Hibernate
本系列是学习笔记系列的第一篇。由于我们公司使用的是 Spring-Data-JPA,在工作的过程中时常遇到不可解释的魔法,故决定深入的探究一下 Hibernate 与 JPA 的关系以及 Hibernate 的部分实现原理.
什么是 JPA
根据 JSR338的描述:JPA 的全称为 Java Persistence API,即 Java 官方用于规范对关系型数据库的操作而定义的一套 API。可以理解为 JPA 只是一套用于统一操作关系型数据库的接口并且提供一些便于开发人员使用的能力,在 JSR338中被提及的能力中笔者认为比较重要的主要有以下几个:
支持运行存储过程
支持用户自定义类型与自定义 ORM 策略
支持 update 和 delete 的 criteria 操作
支持使用 on 条件的外连接(out join)
支持更加灵活的生成值的策略,支持生成 UUID 类型的值
支持不可变属性和只读实体的规范(specification)
什么是 Spring-Data
Spring-Data 是 Spring 旗下的一系列对数据控制的框架集合。该项目的目标是在力求保留不同数据源独有特性的情况下,统一对不同数据源的操作 API。也许这么聊概念有点抽象了,那官网上的案例举个例子吧。
先介绍一下 Spring-Data-Common 模块中一个重要的接口CrudRepository
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
复制代码
很容易的可以发现CrudRepository
定义的是对数据源的增删改查以及一些基础操作,那么 Spring-Data 正是希望通过这个接口统一对数据源的操作。比如 Spring-Data-Redis(例子来源于官方文档):
@RedisHash("people")
public class Person {
@Id String id;
String firstname;
String lastname;
Address address;
}
------------------------------------------------------------------------------------------
public interface PersonRepository extends CrudRepository<Person, String> {
}
------------------------------------------------------------------------------------------
@Autowired PersonRepository repo;
public void basicCrudOperations() {
Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address("emond's field", "andor"));
repo.save(rand);
repo.findOne(rand.getId());
repo.count();
repo.delete(rand);
}
复制代码
再比如 Spring-Data-Elasticsearch(例子来源于 spring-data-example 为了节约版面略有删减):
@Data
@Builder
@Document(indexName = "conference-index")
public class Conference {
private @Id String id;
private String name;
private @Field(type = Date) String date;
private GeoPoint location;
private List<String> keywords;
}
------------------------------------------------------------------------------------------
// 这里的ElasticsearchRepository是CrudRepository的子接口,目的是提供更加丰富的ES操作
interface ConferenceRepository extends ElasticsearchRepository<Conference, String> {}
------------------------------------------------------------------------------------------
class ApplicationConfiguration {
@Autowired ConferenceRepository repository;、
@PostConstruct
public void insertDataSample() {
// Save data sample
var documents = Arrays.asList(
Conference.builder().date("2014-11-06").name("Spring eXchange 2014 - London")
.keywords(Arrays.asList("java", "spring")).location(new GeoPoint(51.500152D, -0.126236D)).build(), //
Conference.builder().date("2014-12-07").name("Scala eXchange 2014 - London")
.keywords(Arrays.asList("scala", "play", "java")).location(new GeoPoint(51.500152D, -0.126236D)).build(), //
Conference.builder().date("2014-11-20").name("Elasticsearch 2014 - Berlin")
.keywords(Arrays.asList("java", "elasticsearch", "kibana")).location(new GeoPoint(52.5234051D, 13.4113999))
.build(), //
Conference.builder().date("2014-11-12").name("AWS London 2014").keywords(Arrays.asList("cloud", "aws"))
.location(new GeoPoint(51.500152D, -0.126236D)).build(), //
Conference.builder().date("2014-10-04").name("JDD14 - Cracow").keywords(Arrays.asList("java", "spring"))
.location(new GeoPoint(50.0646501D, 19.9449799)).build());
repository.saveAll(documents);
}
}
复制代码
最后是今天的主角 Spring-Data-JPA:
@Entity
@Table(name = "reasons")
public class Reason {
@Id
private String id;
private String name;
private Boolean isDeleted;
private Boolean isDisplay;
private Integer sortNum;
}
------------------------------------------------------------------------------------------
// JpaRepository是CrudRepository的子类,为JPA提供更多的支持
@Repository
public interface ReasonRepository extends JpaRepository<Reason, String> {
List<Reason> findByIsDeletedFalse();
@Query(nativeQuery = true, value = "select * from reasons where is_display=true and is_deleted=false order by sort_num limit :size")
List<Reason> findByIsDeletedFalseAndAndIsDisplayOrderBySortNum(@Param("size") int size);
List<Reason> findAllByIsDeletedFalseOrderBySortNum();
}
------------------------------------------------------------------------------------------
public Reason findOne(String id) {
return crudService.findOne(id);
}
复制代码
深入 Hiberate 与 JPA
Hibernate 是 JPA 的实现,但是很有意思的是 Hibernate 出现在 JPA 之前,不知道 JPA 在制定的时候是否有参考 Hibernate 的实现。JPA 对外提供的主要操作接口是entityManager
,不论是何种的操作最终都会落在entityManger
中完成。下文中讨论的所有话题都为 Hibernate 实现的 JPA。
Hibernate 概览
Hibernate 总体结构
Hibernate 的所有操作都是以一个监听器组的形式在框架内部流转处理的,并且不会落入数据库,而是会保存到具体操作的 ActionQueue 中。ActionQueue 可以理解为具体的 SQL 操作的集合,ActionQueue 是有序的,当我们触发了 flush 事件的时候,ActionQueue 中的 SQL 才会依次落入 DB 中执行。一个大家都比较常见的问题就是 SQL 的产生并不按照程序中的代码执行顺序。如下(这里使用的单元测试借用了 Hibernate 的单元测试代码)
@Test
public void test(){
doInHibernate(this::sessionFactory, session -> {
// 预先准备两个记录
Person person=new Person();
person.setId(0);
session.saveOrUpdate(person);
Person person1=new Person();
person1.setId(1);
session.saveOrUpdate(person1);
});
doInHibernate(this::sessionFactory, session -> {
// 先删除id为1的数据
Person person = session.find(Person.class, 1);
session.remove(person);
// 再更新id为0的数据
Person person1 = session.find(Person.class, 0);
person1.setName("123");
session.saveOrUpdate(person1);
});
}
复制代码
而产生的 SQL 的顺序实际上是
10:31:05,522 DEBUG SQL:144 -
update
Person
set
lastUpdatedAt=?,
name=?
where
id=?
10:31:05,538 DEBUG SQL:144 -
delete
from
Person
where
id=?
复制代码
Hibernate 虽然调换了 SQL 的执行顺序但是它保证最终结果的正确。对于 Hibernate 为什么要调换执行顺序?笔者网上查询到的相关答案都是说和executeBatch
相关,即为了批量执行某一类型的操作提升效率,但笔者没有弄清楚具体是什么原理(希望可以留言讨论)。笔者更倾向于认为这只是 Hibernate 架构的设计而已。
缓存状态管理
理解 Hibernate 首先要了解的是 Hibernate 的一级缓存,其实缓存并不是 Hibernate 定义的而是 JPA 规范中要求实现的,Hibernate 也是通过一级缓存来提高性能并且依据一级缓存来管理数据的。
// org.hibernate.engine.internal.StatefulPersistenceContext
// 一级缓存
private HashMap<EntityKey, Object> entitiesByKey;
private HashMap<EntityUniqueKey, Object> entitiesByUniqueKey;
复制代码
一级缓存在 Hibernate 源码中存在两种称呼firstCache
和sessionLevelCache
这里的 EntityKey 和 EntityUniqueKey 可以理解为行记录的 id,也就是说一级缓存实际上是通过记录 ID 去管理的
这里就体现出来一个 Hibernate 比较重要的设计理念即 Hibernate 是希望更加面向对象的去管理数据的。将 hibernate 看做是一种对象管理框架而不是 MyBatis 那样的 SQL 执行框架是理解 Hibernate 设计的核心要义。
Hibernate enables you to develop persistent classes following natural Object-oriented idioms including inheritance, polymorphism, association, composition, and the Java collections framework. Hibernate requires no interfaces or base classes for persistent classes and enables any class or data structure to be persistent.
从一级缓存的结构中我们并没有发现任何一个状态位,那 Hibernate 是如何管理数据的状态的呢?
//org.hibernate.engine.internal.EntityEntryContext
private transient IdentityHashMap<ManagedEntity,ImmutableManagedEntityHolder> immutableManagedEntityXref;
private transient IdentityHashMap<Object,ManagedEntity> nonEnhancedEntityXref;
private ManagedEntity getAssociatedManagedEntity(Object entity) {
// ManagedEntity的维护使用的是IdentityHashMap 也就是说只有 a==b 的时候才会被匹配的到 这就说明了ManagedEntity并不是使用entity的id匹配的
// 在上下文中希望得到entity对应的状态的话使用的 必须是当前的对象而不是能是id一样的另一个对象
// ManagedEntity这个对象实际上 存储了EntityEntry和实际的 操作对象
// EntityEntry并不存储实际的操作对象 只存储对应的状态 如state
if ( ManagedEntity.class.isInstance( entity ) ) {
final ManagedEntity managedEntity = (ManagedEntity) entity;
if ( managedEntity.$$_hibernate_getEntityEntry() == null ) {
// it is not associated
return null;
}
final AbstractEntityEntry entityEntry = (AbstractEntityEntry) managedEntity.$$_hibernate_getEntityEntry();
if ( entityEntry.getPersister().isMutable() ) {
return entityEntry.getPersistenceContext() == persistenceContext
? managedEntity // it is associated
: null;
}
else {
// if managedEntity is associated with this EntityEntryContext, then
// it will have an entry in immutableManagedEntityXref and its
// holder will be returned.
return immutableManagedEntityXref != null
? immutableManagedEntityXref.get( managedEntity )
: null;
}
}
else {
return nonEnhancedEntityXref != null
? nonEnhancedEntityXref.get( entity )
: null;
}
}
复制代码
Hibernate 对状态为的管理使用到了 IdentityHashMap,这是一种特殊的 HashMap,只有传入对象a==key
的时候会匹配出 value。从这里我们可以再次验证上文中提到的那个设计理念:**Hibernate 对数据的管理是基于对象的,即一个关系就是一个对象。**这一点需要特别留意,如果理解的不够深刻可能会让我们的代码遭受到魔法攻击。
缓存状态
Hibernate 中缓存状态分为 4 种类形
Translent: 可以理解为刚刚 new 出来的对象,该对象与 Hibernate 暂时没有任何关系。
Managed: 这个状态下的对象是被 Hibernate 托管的,在最终执行 flush 方法时会将一级缓存中的值写入数据库中。
Removed: 当对象在代码中被 remove 的时候进入该状态,在执行 flush 方法的时候会从数据库中 delete 该值。
Detached: 可以理解为该状态为手动将对象从 hibernate 中解绑的时候进入该状态。
可以看得出来 Hibernate 是通过缓存的状态来管理数据库中的数据,但如果我们使用的不当可能会出现一些奇怪的问题,比如如下代码:
doInHibernate(this::sessionFactory, session -> { // 先将id为1的记录加载进缓存中 Person person = session.find(Person.class, 1); // 重新new一个对象id为1并且尝试save Person person1 = new Person(); person1.setId(1); session.saveOrUpdate(person1); });报错信息: A different object with the same identifier value was already associated with the session : [org.hibernate.jpa.test.callbacks.PreUpdateDirtyCheckingInterceptorTest$Person#1]org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session
复制代码
报错信息中说明的很明显在一级缓存中存在了同样的 key 的数据,并且该数据还已经被一级缓存管理,然而一级缓存中的 entity 和将要保存的数据不是一个对象,框架内部认定这是一个错误情况。
这里的情况如果从源码的角度上分析,就是private HashMap<EntityKey, Object> entitiesByKey
中可以找到对应的记录,但是在private transient IdentityHashMap<Object,ManagedEntity> nonEnhancedEntityXref
中却找不到造成的。用通俗的话说就是一级缓存存在,然而一级缓存对应的状态却不存在
Hibernate 的 saveOrUpdate
为了进一步的理解状态的流转,这里我们以 Hibernate 的 Session 为例子。跟随代码我们可以得到以下的一段关键逻辑
// org.hibernate.event.internal.DefaultSaveOrUpdateEventListener#performSaveOrUpdate protected Serializable performSaveOrUpdate(SaveOrUpdateEvent event) { EntityState entityState = EntityState.getEntityState( event.getEntity(), event.getEntityName(), event.getEntry(),// 这个参数是调用方从session状态缓存中取出来的 event.getSession(), null ); // 通过一级缓存的状态区分save还是update switch ( entityState ) { case DETACHED: // update entityIsDetached( event ); return null; case PERSISTENT: // 只是做基本的检查,可以理解为什么也没有做 因为已经存储到上下文中了 return entityIsPersistent( event ); default: //TRANSIENT or DELETED // save return entityIsTransient( event ); } }
复制代码
EntityState 是 Hibernate 内部用于管理实体对象的状态抽象具体有TRANSIENT,DELETED,PERSISTENT,DETACHED
这几个值
继续深入EntityState.getEntityState
方法
// org.hibernate.event.internal.EntityState#getEntityStatepublic static EntityState getEntityState( Object entity, String entityName, EntityEntry entry, SessionImplementor source, Boolean assumedUnsaved) { // 首先如果状态缓存中存在这么一个状态直接根据状态判断即可 if ( entry != null ) { if ( entry.getStatus() != Status.DELETED ) { if ( LOG.isTraceEnabled() ) { LOG.tracev( "Persistent instance of: {0}", EventUtil.getLoggableName( entityName, entity ) ); } return PERSISTENT; } if ( LOG.isTraceEnabled() ) { LOG.tracev( "Deleted instance of: {0}", EventUtil.getLoggableName( entityName, entity ) ); } return DELETED; } // 在这里负责判断是否是瞬时态 if ( ForeignKeys.isTransient( entityName, entity, assumedUnsaved, source ) ) { if ( LOG.isTraceEnabled() ) { LOG.tracev( "Transient instance of: {0}", EventUtil.getLoggableName( entityName, entity ) ); } return TRANSIENT; } if ( LOG.isTraceEnabled() ) { LOG.tracev( "Detached instance of: {0}", EventUtil.getLoggableName( entityName, entity ) ); } return DETACHED; }
复制代码
这段代码的逻辑非常清晰,让我们继续深入ForeignKeys.isTransient
方法
// org.hibernate.engine.internal.ForeignKeys#isTransientpublic static boolean isTransient(String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) { if ( entity == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { return false; } // 用户可以自定义session的拦截器来操纵这个过程 Boolean isUnsaved = session.getInterceptor().isTransient( entity ); if ( isUnsaved != null ) { return isUnsaved; } // 如果用户没有自定义的session拦截器或者是拦截器放行,则交给实体的持久化器来决定 final EntityPersister persister = session.getEntityPersister( entityName, entity ); isUnsaved = persister.isTransient( entity, session ); if ( isUnsaved != null ) { return isUnsaved; } // 如果持久化器也无法下决断 这时可以有一个兜底的标志位 为了防止请求打在数据库上 if ( assumed != null ) { return assumed; } // 最终请求数据库 并在session域缓存下这次请求数据库的结果 这里并没有放入一级缓存!! // 放入的是另一个名为entitySnapshotsByKey的数据库快照缓存中 final Object[] snapshot = session.getPersistenceContextInternal().getDatabaseSnapshot( persister.getIdentifier( entity, session ), persister ); return snapshot == null; }
复制代码
这段逻辑也十分清晰,继续深入 persister.isTransient 方法
public Boolean isTransient(Object entity, SharedSessionContractImplementor session) throws HibernateException { final Serializable id; if (canExtractIdOutOfEntity()) { //获取ID id = getIdentifier(entity, session); } else { id = null; } if (id == null) { // 假设ID为null 则一定是一个瞬时对象 return Boolean.TRUE; } final Object version = getVersion(entity); if (isVersioned()) { // 这里在检查版本,如果有用@Version标注了某个字段那么就有版本的概念 // 检查这个版本是否为未保存,未保存就是瞬时对象 Boolean result = entityMetamodel.getVersionProperty() .getUnsavedValue().isUnsaved(version); if (result != null) { return result; } } Boolean result = entityMetamodel.getIdentifierProperty() .getUnsavedValue().isUnsaved(id); // 检查这个ID是否未保存 如果未保存就是瞬时态 如果无法断定这里返回null // 在我们不指定unsave-value属性的情况下这里固定为UnDefined 返回值固定为null if (result != null) { return result; } // 最后去二级缓存中查找一下 if (session.getCacheMode().isGetEnabled() && canReadFromCache()) { final EntityDataAccess cache = getCacheAccessStrategy(); final Object ck = cache.generateCacheKey(id, this, session.getFactory(), session.getTenantIdentifier()); final Object ce = CacheHelper.fromSharedCache(session, ck, getCacheAccessStrategy()); if (ce != null) { return Boolean.FALSE; } } // persister 归根结底只是在内存和缓存中判断 // 如果不能明确的知道对象是否为瞬时态 最终返回null交于上层判断 return null; }
复制代码
这个流程的流程图如下所示
可以发现 Hibernate 在判断缓存状态的时候选择的策略总体上是尽可能的不发起 IO,并且为用户做了一定的自定义拓展。其中自由度最高的是拦截器的拓展,但是拦截器是适用于全局的,也就是没办法精确到具体的某一张表做拦截,这样的拦截粒度在大多数的场景下是比较尴尬的。
Spring-Data-JPA
Spring-Data-JPA 是强依赖 Hibernate 的一个项目,目的是为了简化 JPA 的编写,由于底层的实现是 Hibernate,所以这里就挑一些开发中常见的问题来进行说明。
save 方法
与上文中对应的是 JPA 的 Repository 中有一个 save 方法,这是 Spring-Data-JPA 提供的用于保存数据的方法。
public <S extends T> S save(S entity) { Assert.notNull(entity, "Entity must not be null."); if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } }
复制代码
针对这里的 isNew 方法的实现在官方文档中有如下的介绍
根据 ID 和 version 字段(默认策略): 默认 Spring Data JPA 会优先判断是否有非原生类型的 version 属性,如果有那么当这个 entity 的 version 字段为 null 的时候我们认为她是new
的。如果没有这个 version 属性,那么 ID 属性为 null 的 entity 被认为是new
的,其余情况统一判断为非new
。
实现Persistable
接口: 如果一个 entity 实现了Persistable
接口,框架会通过这个接口提供的isNew
方法判断。
public interface Persistable<ID> { @Nullable ID getId(); boolean isNew();}
复制代码
public interface EntityInformation<T, ID> extends EntityMetadata<T> { boolean isNew(T var1); @Nullable ID getId(T var1); default ID getRequiredId(T entity) throws IllegalArgumentException { Assert.notNull(entity, "Entity must not be null!"); ID id = this.getId(entity); if (id != null) { return id; } else { throw new IllegalArgumentException(String.format("Could not obtain required identifier from entity %s!", entity)); } } Class<ID> getIdType();}
复制代码
相比于 Hibernate 原生的定义,这里方法则丰富的多,支持不同维度的处理。
注意:我们不可将persist
和merge
与 SQL 之间做关联,使用 JPA 的时候请脱离 SQL 思维!!
反思
答: 先说答案因为一级缓存没有刷新。在进行原生的 SQL update 的时候会触发 flush 事件,flush 事件会将一级缓存中的数据和 ActionQueue 中的内容刷新到数据库中,但不会刷新一级缓存。那不刷新一级缓存是正确的吗?笔者的答案是:是合理的,由于原生的 SQL 进行 update 框架并不能感知到具体是哪个对象被 update 了。如果这个时候清理了一级缓存,可能会造成巨大的性能损失,想象一下如果一级缓存中有 5000 个对象,我的 SQL 可能只更新了其中一个对象但是整个一级缓存被我清理掉了,这会让接下来的查询都发起 select 语句向数据库请求。
那如何处理比较合适?
如果是使用@Modifying
注解在 Repository 中标注的方法执行的语句可以适当的调整注解中的两个参数:
@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })@Documentedpublic @interface Modifying { boolean flushAutomatically() default false; boolean clearAutomatically() default false;}
复制代码
如果是使用entityManager
中的方法createNativeQuery
生成的原生 update 语句,请适当的使用entityManager.detach
方法对一级缓存进行清理,但需要再次强调的是:使用语句对数据库更新是极其违背 JPA 设计初衷的
答: 如果支持这样的生成策略就违背了 JPA 的设计初衷: JPA 是面向对象的。它让数据库的关系与 OOP 的 Object 映射,并且让开发人员通过 Object 无感的管理数据库中的关系。既然是面向对象的,就应该是以对象的维度进行增删改查,而不是以某一个字段的维度进行操作。
总结
JPA 是作为 EJB 的一个补充项目
使用 JPA 的时候需要注意一级缓存的中对象需要与上下文中需要操作的业务对象一致
一级缓存由一个 HashMap 存储 key 为记录的主键,一级缓存的状态由一个 IdentityHashMap 管理,key 为一级缓存中的对象本身。
请将 JPA 当做一个对象容器使用,我们的每一次 CRUD 都是在修改对象而不是修改记录。不要把它当做类似于 MyBatis 这样的 SQL 操作框架
正是因为 JPA 是以对象的形势统一管理记录的,所以直接通过 SQL 或任何尝试生成 update 语句对数据进行修改的操作都是不可靠的(虽然 JPA 有提供对应的实现,但这个仅仅是为了兼容操作)
本文 Hibernate 版本 5.5.2.Final,Spring-Data-JPA 版本 2.6.0-SNAPSHOT
评论 (1 条评论)