hibernate-基础
hibernate 作为 ORM 解决方案,非常强调领域模型的概念,是非侵入式的,Hibernate不要求持久化类实现任何接口或继承任何类,POJO即可
hibernate 官方文档-https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html
hibernate 总体的架构图 如下图所示,hibernate 实现了java persistence api(JPA),同时也自己扩展了接口 hibernate native api
graph LR
userDataAccessLayer(user data access layer)
javaPersistenceApi(java persistence api)
hibernateNativeApi(hibernate native api)
hibernate
jdbc
database
userDataAccessLayer -.-> javaPersistenceApi
userDataAccessLayer -.-> hibernateNativeApi
subgraph hibernate-core
javaPersistenceApi -.-> hibernate
hibernateNativeApi -.-> hibernate
end
subgraph database-jdbc
hibernate-.->jdbc
jdbc-.->database
end
style hibernate-core fill: #f9f, stroke: #333, stroke-width: 1px;
style database-jdbc fill: #ee9f, stroke: #333, stroke-width: 1px;
Hibernate API 最基础的 API
-
SessionFactory(会话工厂) 是一个线程安全的,immutable(终态的),它是一个代理,表示应用程序域,SessionFactory 的建立代价很大,所以一个 应用只能有一个 SessionFactory,SessionFactory 维护 ,Hibernate 的所有 Session(会话),二级缓冲,连接池,事务等等。如果应用需要使用Hibernate访问多个数据库,则需要对每一个数据库使用一个SessionFactory。
-
Session(会话) Session(会话)是一个单线程,短生命周期的对象,是按"Unit of Work(工作单元),Session 维护了一级缓存,在第一次发送SQL语句查询后第二次直接使用缓存中的数据,不会再发送SQL。除非session缓存被清空,平常的 crud 就通过它。session是一个轻量级对象,Session实例并不是线程安全的,因此应该被设计为每次只能在一个线程中使用。通常将每一个Session实例和一个数据库事务绑定,也就是说,每执行一个数据库事务,都应该先创建一个新的Session实例
-
Transaction(事务) 那么每个Session的操作,是一个独立的事务,是Hibernate的数据库事务接口,它对底层的事务接口做了封装,底层事务接口包括JDBC事务和JTA(Java Transaction API)事务
-
Query (接口) 是Hibernate的查询接口,用于向数据库查询对象,以及控制执行查询的过程。Query实例包装了一个HQL(Hibernate Query Language)查询语句,HQL查询语句与SQL查询语句有些相似,但HQL查询语句是面向对象的,它引用类名及类的属性名,而不是表名及表的字段名。 Query接口让你方便地对数据库及持久对象进行查询,它可以有两种表达方式:HQL语言或本地数据库的SQL语句。Query经常被用来绑定查询参数、限制查询记录数量,并最终执行查询操作。
-
Criteria(接口)
-
Configuration(配置类) Configuration 类的作用是对Hibernate 进行配置,以及对它进行启动。在Hibernate 的启动过程中,Configuration 类的实例首先定位映射文档的位置,读取这些配置,然后创建一个SessionFactory对象。虽然Configuration 类在整个Hibernate 项目中只扮演着一个很小的角色,但它是启动hibernate 时所遇到的第一个对象。
如图中的根接口都是在 jpa 中定义的
classDiagram
I_EntityManagerFactory <|-- I_SessionFactory
I_SessionFactory <|.. C_SessionFactoryImpl
I_EntityManager <|-- I_Session
I_Session <|.. C_SessionImpl
I_EntityTransaction <|-- I_Transaction
I_Transaction <|.. C_TransactionImpl
环境准备
mysql 测试通过,postgres 用的是 42.5.1 版本 测试没有通过 ,无法加载 postgres drive
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--hibernate-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.14.Final</version>
</dependency>
基础类
class Config {
/**
* 输出所有管理的实体
* @param context
* @return
*/
public static Map.Entry<Object, EntityEntry>[] getManagedEntities(PersistenceContext context) {
var map = context.reentrantSafeEntityEntries();
return map;
}
public void extracted(Consumer<Configuration> configurer, Consumer<Session> handler) {
var cfg = new Configuration();
//.configure() 在classpath根路径下搜索名为hibernate.cfg.xml的文件,如果没有找到将会抛出异常,
//也可以指定 configure 方法参数为指定的路径
//var cfg = new Configuration().configure();
/*
hibernate.cfg.xml
<hibernate-configuration>
<session-factory>
<property name="xxx">yyy</property>
//数据库和类映射 xml 文件
<mapping resource="xxx.hbm.xml"/>
</session-factory>
</hibernate-configuration>
*/
// dialect 指明具体联系的数据库类型,因为同一个数据库因为版本的不同,标准也会有差异, SQL
//标准各个厂家完全遵守的
cfg.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
cfg.setProperty("hibernate.connection.driver_class", "com.mysql.cj.jdbc.Driver");
cfg.setProperty("hibernate.connection.url", "jdbc:mysql://localhost:3306/MyDB");
cfg.setProperty("hibernate.connection.username", "root");
cfg.setProperty("hibernate.connection.password", "root");
cfg.setProperty("hibernate.show_sql", "true");
cfg.setProperty("format_sql", "true");
cfg.setProperty("use_sql_comments", "true");
cfg.setProperty("hibernate.hbm2ddl.auto","update");
//设置全局拦截器
cfg.setInterceptor(new MyInterceptor());
//外部继续配置
configurer.accept(cfg);
SessionFactory factory = null;
Session session = null;
//ServiceRegistry 是 Service 的注册表, 它为Service提供了一个统一的加载 / 初始化 / 存放 / 获取机制.
var serviceRegistry=new StandardServiceRegistryBuilder().
applySettings(cfg.getProperties()).build();
try {
factory = cfg.buildSessionFactory(serviceRegistry);
var ii= factory.unwrap(SessionFactoryImplementor.class);
ii.getServiceRegistry().getService(EventListenerRegistry.class).
appendListeners(EventType.FLUSH,new MyEvent());
session = factory.openSession();
if (session.isConnected()) {
System.out.println("连接已通");
//关于 session 的测试
handler.accept(session);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (session != null) {
session.close();
}
if (factory != null) {
factory.close();
}
}
}
}
class MyInterceptor extends EmptyInterceptor {
/**
* 脏数据检查,可以知道那些字段发什么了什么变化
* @param entity
* @param id
* @param currentState
* @param previousState
* @param propertyNames
* @param types
* @return
*/
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
Object[] previousState, String[] propertyNames, Type[] types) {
for (int i = 0; i < propertyNames.length; i++) {
var type = types[i];
var newValue= currentState[i];
var oldValue= previousState[i];
System.out.println("Cargo 的属性:"+propertyNames[i] +
", 属性类型:"+type.getName()+
", id:"+id+
", old value:"+oldValue+
", new value:"+newValue);
//在持久化到数据库之前,还可以再次修改属性值
}
//返回 true ,在此方法里对 currentState 的修改会持久化到数据库.
//返回 false, 不会把对 currentState 修改持久化到数据库
return false;
}
}
简单的测试类
public class Programmer {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
var config =new Config();
config.extracted(cfg -> {
/*
可以代替 xml 方式的映射文件 <mapping resource="xxx.hbm..xml" /> 持久化类
如果有多个实体类,一个个添加是不是很麻烦
只能是自己去封装了
*/
cfg.addAnnotatedClass(Cargo.class);
cfg.addAnnotatedClass(Person.class);
},
//业务逻辑
session -> {
session.beginTransaction();
var cargo = session.get(Cargo.class,1);
var context = session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
session.getTransaction().commit();
});
}
脏数据
对被 session 管理的数据进行更改后的数据是脏数据,也就是和数据库的数据不同步了,当提交事务的时候,脏数据会自动同步到数据库
session.beginTransaction();
var cargo = session.get(Cargo.class,1);
var context = session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
System.out.println("数据是否改变"+session.isDirty());
//false
cargo.setName("dirt");
System.out.println("数据是否改变"+session.isDirty());
//true
session.getTransaction().commit();
System.out.println("数据是否改变"+session.isDirty());
//false
/*
连接已通
Hibernate: select cargo0_.id as id1_0_0_,
cargo0_.name as name2_0_0_ from Cargo cargo0_ where cargo0_.id=?
管理的实体个数:1
com.hank.entity.Cargo@3d1f558a
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
数据是否改变false
Cargo 的属性:name, 属性类型:string, id:1, old value:bag, new value:dirt
数据是否改变true
Cargo 的属性:name, 属性类型:string, id:1, old value:bag, new value:dirt
Hibernate: update Cargo set name=? where id=?
数据是否改变false
*/
JPA 和 Hibernate API 通过 unwrap 互相转
var context = session.unwrap(SessionImplementor.class).getPersistenceContext();
var entityManager =session.unwrap(EntityManager.class);
var sessionFactory=session.getEntityManagerFactory().unwrap(SessionFactory.class);
对象的状态
session 的主要工作就是让实体对象在不同的状态之间进行互相切换
-
瞬时状态:实体-没有ID,没有与Session关联
-
持久化状态:实体-有ID,与Session关联,特点:持久化状态对象的任何改变都会同步到数据库中 在Hibernate中,持久对象是具有数据库标识且在Session中的实例,持久对象有一个主键值设为数据库标识符。持久对象可能来自瞬时对象被save或saveOrUpdate之后形成,也可能是通过get()或load()等方法从数据库中检索出来的对象。
-
游离态:实体-有ID,没有与Session关联.游离态实例表示曾经与某个持久化上下文发生过关联,不过那个上下文被关闭了。它拥有持久化标识,并且在数据库中通常还存在一个对应的行,只是它已经不在持久化层的管理之下。
-
删除状态
EntityEntry 上下文跟踪的实体信息对象
脏数据及对象的缓存
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
var cargo = session.get(Cargo.class,1);
cargo.setName("tool");
System.out.println("session 是否有脏数据:"+session.isDirty());
//属性缓存的作用,根据缓存的状态,提前生成SQL 语句,但是不会执行,执行要提交事务的时候
//会生成 更新语句,同时会更新缓存的状态
session.flush();
System.out.println("session 是否有脏数据:"+session.isDirty());
//会在缓存里找数据
var cargo1 = session.get(Cargo.class,1);
System.out.println(cargo.equals(cargo1));
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
session.getTransaction().commit();
清除管理的实体
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
var cargo = session.get(Cargo.class,1);
cargo.setName("tools");
System.out.println("session 是否有脏数据:"+session.isDirty());
//清除 session 管理的所有实体
session.clear();
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
var cargo1 = session.get(Cargo.class,1);
System.out.println(cargo1.getName());
System.out.println("是否是缓存:"+cargo.equals(cargo1));
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
/*
管理的实体个数:0
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_
from Cargo cargo0_ where cargo0_.id=?
session 是否有脏数据:true
管理的实体个数:0
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_
from Cargo cargo0_ where cargo0_.id=?
tool
是否是缓存:false
管理的实体个数:1
com.hank.entity.Cargo@4f4c88f9
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
*/
evict 在 session 缓存中清除一个持久化对象
var cargo=session.get(Cargo.class,1);
session.evict(cargo);
//jpa 方法 detach 同样的效果
//session.detach(cargo);
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
//1
session 对象
get , load 区别
get 立即加载,load 延迟加载
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
//立即加载
var cargo1=session.get(Cargo.class,1);
//懒加载,在使用属性的时候才会加载
var cargo2=session.load(Cargo.class,2);
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
//使用属性的时候才加载
System.out.println(cargo2);
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
session.getTransaction().commit();
/*
管理的实体个数:0
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_
from Cargo cargo0_ where cargo0_.id=?
管理的实体个数:1
com.hank.entity.Cargo@4f327096
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_
from Cargo cargo0_ where cargo0_.id=?
com.hank.entity.Cargo@75e09567
管理的实体个数:2
com.hank.entity.Cargo@4f327096
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
com.hank.entity.Cargo@75e09567
EntityEntry[com.hank.entity.Cargo#2](MANAGED)
*/
update , merge 区别
注意点:
-
update 更新 dispatch 对象,此对象会变成持久对象 更新 persistent 对象没有意义 更新 transient 对象,如果此对象有id,更新后除了设定更新的字段,其他字段会重置成null,无id 不能执行会报错
-
merge 更新 transient 对象,如果此对象有id,数据库有记录就进行更新数据库,无记录插入数据 更新 persistent 对象没有意义
save , persist 区别
persist 不会立即执行sql,所以拿不到数据库返回的 id ,save 会立即执行 sql,可以立即拿到数据库返回的 id
批量更新
批量更新,并且不会再访问的对象。具体的做法是在处理完一个对象或小批量对象后,立刻调用flush()方法清理缓存,然后再调用clear()方法 或 evict 方法清空缓存
普通的批量更新
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
//批量更新,如果批量数据比较大,缓存里不清空是比较危险的
for (int i = 1; i < 9; i++) {
var cargo=session.get(Cargo.class,i);
cargo.setName(LocalDateTime.now().toString());
//生成 sql 语句
session.flush();
// 删除缓存里的记录
session.evict(cargo);
}
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
//一次性把所有 sql 语句发给数据库
session.getTransaction().commit();
});
通过 HQL 的批量操作,是在数据库完成 .scroll 方法返回的是游标, 只有真正访问对象的时候才会去查询对象
//返回的是游标
var scroll= session.createQuery("from Cargo ").scroll(ScrollMode.FORWARD_ONLY);
while (scroll.next()){
//查询对象
var cargo =(Cargo) scroll.get()[0];
cargo.setName(LocalDateTime.now().toString());
session.flush();
session.clear();
}
spring data jpa
需要的依赖
父模块引入 spring-data-bom 方便子模块的版本管理
<!-- 在父级声明需要的依赖,通过这种关系可以引入多个父类 parent 标签只能引入一个父类 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2021.1.10</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
子模块
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.14.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.7.5</version>
</dependency>
实体类
@Entity
@Getter
@Setter
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "name", nullable = true)
String name;
}
spring data jpa 配置类
@Component
public class ConfigerDB {
/**
* 配置数据源
*
* @return
*/
@Bean("dataSource")
public DataSource getDataSource() {
var dataSource = DataSourceBuilder.create();
dataSource.url("jdbc:mysql://localhost:3306/MyDB");
dataSource.driverClassName("com.mysql.cj.jdbc.Driver");
dataSource.username("root");
dataSource.password("root");
//连接池,演示目的不设置参数了
var pool = new HikariDataSource();
pool.setDataSource(dataSource.build());
return pool;
}
/**
* 配置 jpa 属性
* @return
*/
@Bean("jpaProperties")
public JpaProperties getJpaProperties() {
var jpaProperties = new JpaProperties() {
};
jpaProperties.setShowSql(true);
//设置属性
jpaProperties.setProperties(new HashMap<>() {
{
put("hibernate.hbm2ddl.auto", "update");
put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
put("hibernate.show_sql", "true");
}
});
return jpaProperties;
}
/**
* 提供 jpa 的实现者
* spring boot 默认就包含了 Hibernate 的包
* Hibernate对Jpa的实现
* @return
*/
@Bean("jpaVendorAdapter")
public JpaVendorAdapter getJpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
/**
* 实体管理工厂
* @param dataSource
* @param jpaProperties
* @param jpaVendorAdapter
* @return
*/
@Bean("myEntityManagerFactory")
@Primary
public LocalContainerEntityManagerFactoryBean getEntityManagerFactory(
DataSource dataSource, JpaProperties jpaProperties, JpaVendorAdapter jpaVendorAdapter) {
var localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
localContainerEntityManagerFactoryBean.setDataSource(dataSource);
localContainerEntityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
localContainerEntityManagerFactoryBean.setJpaPropertyMap(jpaProperties.getProperties());
localContainerEntityManagerFactoryBean.setPersistenceUnitName("myPersistenceUnitName");
//指定扫描的实体所在的包路径
localContainerEntityManagerFactoryBean.setPackagesToScan("com.hank.entityModel");
return localContainerEntityManagerFactoryBean;
}
/**
* spring 需要的事务
* @param myEntityManagerFactory
* @return
*/
@Bean("jdbcTransactionManager")
public JpaTransactionManager getJdbcTransactionManager(
LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
return new JpaTransactionManager(myEntityManagerFactory.getObject());
}
}
按照 JPA 的方式使用
如下代理在 spring jpa 里可以完全按照 jpa 规范使用
@ComponentScan({"com.hank"})
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
{
//这里不能通过字符串获取 bean ,因为这样获取到的是代理对象
var localContainerEntityManagerFactoryBean = context.getBean(
LocalContainerEntityManagerFactoryBean.class);
//读取数据并在事务里修改数据
{
var entityManager =localContainerEntityManagerFactoryBean.getObject().createEntityManager();
var transaction=entityManager.getTransaction();
transaction.begin();
System.out.println(entityManager);
var person= entityManager.find(Person.class,1);
System.out.println(person.getName());
person.setName(LocalDateTime.now().toString());
transaction.commit();
entityManager.close();
}
//读取上面事务代码修改的数据
{
var entityManager =localContainerEntityManagerFactoryBean.getObject().createEntityManager();
System.out.println(entityManager);
var person= entityManager.find(Person.class,1);
System.out.println(person.getName());
entityManager.close();
}
}
}
}
/*
SessionImpl(518349613<open>)
2022-12-07T11:14:06.420353
SessionImpl(5998675<open>)
2022-12-07T11:30:04.118294800
*/
测试 spring data 里的事务
注意点:
- @PersistenceContext 获取 EntityManager 代理,是线程安全的
- @PersistenceUnit 获取 EntityManagerFactory
测试用例
@Component
class Dao {
/**
* 这里获取的是 EntityManager 代理
* Shared EntityManager proxy for target factory
* [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250]
* 如果有多个持久化单元,需要指定持久化单元的名字 --myEntityManagerFactory
*/
@PersistenceContext(unitName = "myEntityManagerFactory")
private EntityManager entityManager;
/**
* org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250
* 如果有多个持久化单元,需要指定持久化单元的名字 --myEntityManagerFactory
*/
@PersistenceUnit(unitName = "myEntityManagerFactory")
private EntityManagerFactory entityManagerFactory;
@Transactional("jdbcTransactionManager")
public void func1() {
System.out.println(entityManager);
//entityManager 是代理对象
//entityManager 代理对象不能像 func2 那样使用事务,需要按照 spring 事务的使用方式使用
var person = entityManager.find(Person.class, 1);
person.setName(LocalDateTime.now().toString());
System.out.println("设置数值:" + person.getName());
entityManager.close();
}
/**
* 验证 func1 事务
* 在本方法里手动开启事务
*/
public void func2() {
System.out.println(entityManagerFactory);
var entityManager = entityManagerFactory.createEntityManager();
System.out.println(entityManager);
//SessionImpl(1337346642<open>)
//此处不是代理对象,不能使用 spring 的事务需要自己开启事务
var transaction=entityManager.getTransaction();
transaction.begin();
var person = entityManager.find(Person.class, 1);
System.out.println("读取数值:" + person.getName());
person.setName(LocalDateTime.now().toString());
System.out.println("设置数值:" + person.getName());
transaction.commit();
entityManager.close();
}
/**
* 验证 func2 手动开启的事务
*/
public void func3() {
System.out.println(entityManagerFactory);
var entityManager = entityManagerFactory.createEntityManager();
System.out.println(entityManager);
//SessionImpl(789367604<open>)
var person = entityManager.find(Person.class, 1);
System.out.println("读取数值:" + person.getName());
entityManager.close();
}
}
测试
@ComponentScan({"com.hank"})
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
context.getBean(Dao.class).func1();
context.getBean(Dao.class).func2();
context.getBean(Dao.class).func3();
}
/*
Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250]
设置数值:2022-12-07T11:14:06.381306600
org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250
SessionImpl(1337346642<open>)
读取数值:2022-12-07T11:14:06.381306600
设置数值:2022-12-07T11:14:06.420353
org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250
SessionImpl(789367604<open>)
读取数值:2022-12-07T11:14:06.420353
*/
}
spring data jpa 拓展
Repository 及其子类
Repository<T,ID> 位于Spring Data Common的lib里面,是Spring Data 里面做数据库操作的最底层的抽象接口、最顶级的父类,源码里面其实什么方法都没有,仅仅起到一个标识作用。管理域类以及域类的id类型作为类型参数,此接口主要作为标记接口捕获要使用的类型,并帮助你发现扩展此接口的接口。能够在类路径扫描期间发现扩展该类型的接口.Spring底层做动态代理的时候发现只要是它的子类或者实现类,都代表储存库操作
注意:
- @NoRepositoryBean 在接口或对象上加此注解的 jpa 存储库,不会注入 spring ioc
- @Repository 如果在加了 @NoRepositoryBean 接口的实现类注入 spring ioc ,要把 @Repository 加载类或接口上
- 为什么存储库的方法只用会有事务,因为 SimpleJpaRepository 实现类上加了注解 @Transactional
Repository 大类:
- ReactiveCrudRepository,响应式编程,主要支持当前 NoSQL 方面的操作,因为这方面大部分操作都是分布式的,目前 Reactive 主要有 Cassandra、MongoDB、Redis 的实现。
- CoroutineCrudRepository :为了支持 Kotlin 语法而实现的。
- CrudRepository :JPA 相关的操作接口,也是我们主要用到的接口
更详细一点,我们需要掌握和使用到的7 大 Repository 接口如下所示:
- Repository(org.springframework.data.repository),没有暴露任何方法;
- CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
- PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
- QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
- JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
- JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;
两大 Repository 实现类:
- SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类;
第三方的 QueryDsl 在 JPA 和 spring data 的基础上扩展的第三方查询库:
- 接口 QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装
- 实现 QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类
spring 扩展的方法
@NamedQuery、@Query和方法定义查询的对比:
- Spring JPA里面的优先级,@Query > @NameQuery > 方法定义查询。
- 推荐使用的优先级:@Query > 方法定义查询 > @NameQuery。
- 相同点是都不支持动态条件查询。
方法的查询策略设置 @EnableJpaRepositories 注解来配置方法的查询策略,详细配置方法如下: @EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:
- Create:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则, 启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有用的。
- USE_DECLARED_QUERY:声明方式创建,启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,可以理解为必须配置 @Query 或 @NameQuery
- CREATE_IF_NOT_FOUND:这个是默认的,除非有特殊需求,可以理解为这是以上 2 种方式的兼容版。先用声明方式(@Query 或 @NameQuery)进行查找,如果没有找到与方法相 匹配的查询,那用 Create 的方法名创建规则创建一个查询;这两者都不满足的情况下,启动就会报错。
通过 @EnableJpaRepositories 注解需要提供 Repositories 包路径,entityManagerFactory 的beanName,transactionManager 的 beanName 此注解会扫描 Repository 及其子接口,并为其生成代理类,我们操作的是代理类
Declared Queries -方法定义查询 ,如果返回实体,不能返回实体的部分属性,而是全部属性
方法定义查询的方法要定义在仓储接口里,有代理类用相应的接口的实现类去实现接口内自定义方法
//@Indexed 提升 @CompoentScan 扫描的效率
//spring jpa 定义的 Repository 是个空接口
@Indexed
public interface Repository<T, ID> {
}
public interface MyPersonRepository extends Repository<Person, Integer> {
//方法定义查询,要以固定的前缀开头加属性表达式结尾
public Person findDistinctByName(String name);
}
这种方法个人感觉不好用,属于死记硬背的做法,而且写错也没办法在编译的时候,检查出错误
属性表达式(Property Expressions)(findByAddressZipCode) 只能引用托管(泛化)实体的直接属性,如前一个示例所示。在查询创建时,你已经确保解析的属性是托管实体的属性。同时,还可以通过遍历嵌套属性定义约束。假设一个Person实体对象里面有一个Address属性里面包含一个ZipCode属性。在这种情况下,方法名为:
List<Person> findByAddressZipCode(string zipCode)
//或
List<Person> findByAddress_ZipCode(string zipCode)
声明的查询支持的前缀
关键词 | 说明 |
---|---|
find, get, query, stream | 查询所有的字段 |
count | 统计数量 |
exists | 是否存在 |
delete , remove | 移除 |
save | 保存或更新 |
* 声明的查询支持的关键字, 在类 org.springframework.data.repository.query.parser.PartTree 定义*
关键词 | 举例 |
---|---|
Distinct | findDistinctByLastnameAndFirstname |
And | findByLastnameAndFirstname |
Or | findByLastnameOrFirstname |
Is,Equals | findByFirstname, findByFirstnameIs,findByFirstnameEquals |
Between | findByStartDateBetween |
LessThan | findByAgeLessThan |
LessThanEqual | findByAgeLessThanEqual |
GreaterThan | findByAgeGreaterThan |
GreaterThanEqual | findByAgeGreaterThanEqual |
After | findByStartDateAfter |
Before | findByStartDateBefore |
IsNull,Null | findByAge(Is)Null |
IsNotNull,NotNull | findByAge(Is)NotNull |
Like | findByFirstnameLike |
NotLike | findByFirstnameNotLike |
StartingWith | findByFirstnameStartingWith |
EndingWith | findByFirstnameEndingWith |
Containing | findByFirstnameContaining |
OrderBy | findByAgeOrderByLastnameDesc |
Not | findByLastnameNot |
In | findByAgeIn(Collection |
NotIn | findByAgeNotIn(Collection |
True | findByActiveTrue() |
False | findByActiveFalse() |
IgnoreCase | findByFirstnameIgnoreCase |
综上,总结 3 点经验:
- 方法名的表达式通常是实体属性连接运算符的组合,如 And、or、Between、LessThan、GreaterThan、Like 等属性连接运算表达式,不同的数据库(NoSQL、MySQL)可能产生的效果不一样,如果遇到问题,我们可以打开 SQL 日志观察。
- IgnoreCase 可以针对单个属性(如 findByLastnameIgnoreCase(…)),也可以针对查询条件里面所有的实体属性忽略大小写(所有属性必须在 String 情况下,如 findByLastnameAndFirstnameAllIgnoreCase(…))。
- OrderBy 可以在某些属性的排序上提供方向(Asc 或 Desc),称为静态排序,也可以通过一个方便的参数 Sort 实现指定字段的动态排序的查询方法(如 repository.findAll(Sort.by(Sort.Direction.ASC, "myField")))。
通过 @EnableJpaRepositories 注解需要提供 Repositories 包路径,entityManagerFactory 的beanName,transactionManager 的 beanName 此注解会扫描 Repository 及其子接口,并为其生成代理类,我们操作的是代理类
@ComponentScan({"com.hank"})
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
// true
System.out.println(Proxy.isProxyClass(repository.getClass()));
//class com.sun.proxy.$Proxy62
System.out.println(repository.getClass());
//org.springframework.data.jpa.repository.support.SimpleJpaRepository@2fac80a8
//代理对象代理的实现类是 SimpleJpaRepository
System.out.println(repository);
//方法 findByName 的实现在 SimpleJpaRepository 类里
var person =repository.findDistinctByName("ok");
}
}
@Query -JPQL注解查询
@Query 属于JPQL 查询,同时也可以指定原生的 sql 查询方式(nativeQuery = true),除过查询其他方法要加 @Modifying 注解 @Modifying(clearAutomatically = true) 作用是在做更新后立即清除当前缓存,并做一次数据库查询 参数的指定方式有:
- ?1 传入的参数是方法的第一个参数 ,序号参数
- :name 传入的参数是方法的参数上加 @Param("name") 注解的,命名参数
public interface MyPersonRepository extends Repository<Person, Integer> {
//查询部分字段,相比较声明查询的优点
@Query(" select u.name from Person u where u.id=1 and u.name=?1")
public String findByName1(String name);
@Query("from Person u where u.id=1 and u.name=:name")
public Person findByName2(@Param("name") String name);
//原生查询
@Query(value = "select * from Person u where u.id=1 and u.name=:name", nativeQuery = true)
public Person findByName3(@Param("name") String name);
//还可以传入对象 :#{#person.name} 属于 SpEL 表达式
@Query(value = "from Person u where u.id=1 and u.name=:#{#person.name}")
public Person findByName4(@Param("person") Person person);
//更新
@Modifying
@Transactional
@Query(value = "update Person u set u.name=?1 where u.id=?2 ")
public int updatePerson(String name,int id);
//删除
@Modifying
@Transactional
@Query(value = "delete from Person u where u.id=?1")
public int deletePerson(int id);
//新增
@Modifying
@Transactional
@Query(value = "insert into Person (name) values(?1) ",nativeQuery = true)
public int insetPerson(String name);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
// true
System.out.println(Proxy.isProxyClass(repository.getClass()));
//class com.sun.proxy.$Proxy62
System.out.println(repository.getClass());
//org.springframework.data.jpa.repository.support.SimpleJpaRepository@2fac80a8
//代理对象代理的实现类是 SimpleJpaRepository
System.out.println(repository);
System.out.println(repository.findByName1("ok"));
System.out.println(repository.findByName2("ok"));
System.out.println(repository.findByName3("ok"));
System.out.println(repository.findByName4(new Person(){{setName("ok");}}));
System.out.println(repository.updatePerson("ok",1));
System.out.println(repository.deletePerson(1));
System.out.println(repository.insetPerson("bad"));
}
/*
true
class com.sun.proxy.$Proxy66
org.springframework.data.jpa.repository.support.SimpleJpaRepository@5112b7
ok
com.hank.entityModel.Person@333d44f6
com.hank.entityModel.Person@32b112a1
com.hank.entityModel.Person@678586f0
1
1
1
*/
}
SpEL 表达式
在Spring Data JPA 1.4以后,支持在@Query中使用SpEL表达式(简介)来接收变量,这为使用 @Query 提供了更大的便利性
//还可以传入对象 :#{#person.name} 属于 SpEL 表达式
//#{#entity} 表示的是 @Entity 指定的名字会 实体的类名
@Query(value = "from #{#entity} u where u.id=1 and u.name=:#{#person.name}")
public Person findByName4(@Param("person") Person person);
@NamedQuery-命名查询
@NamedQuery注解定义的查询里写的也是 jpql,之相对应的还有@NamedNativeQuery 书写的是原生 sql 这种把查询方法定义在实体的方式用起来不优雅,一般不使用
@Entity
@Getter
@Setter
//name = "Person.myNamedQuery" 用实体名称开头
@NamedQuery(name = "Person.myNamedQuery",query = "select c from Person c where c.id=?1")
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "name", nullable = true)
String name;
}
@NamedStoredProcedureQueries , @Procedure 命名查询的存储过程
@NamedStoredProcedureQueries 必须加载一个实体上 @Procedure 加载方法上
QueryByExampleExecutor 简单化的动态查询
QueryByExampleExecutor 接口实现动态查询创建,并且不需要编写包含字段名称的查询,所谓动态查询是查询的字段不一定有几个,也可以传入排序和分页
提供一个实例作为动态查询条件 Query by Example 也有几个限制:
- 不支持嵌套或分组的属性约束,例如firstname = ?0 or (firstname = ?1 and lastname = ?2)
- 仅支持字符串的 starts/contains/ends/regex 匹配和其他属性类型的精确匹配
public interface QueryByExampleExecutor<T> {
/**
* Returns a single entity matching the given {@link Example} or {@link Optional#empty()}
* if none was found.
*
* @param example must not be {@literal null}.
* @return a single entity matching the given {@link Example} or {@link Optional#empty()}
* if none was found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the Example
* yields more than one result.
*/
<S extends T> Optional<S> findOne(Example<S> example);
/**
* Returns all entities matching the given {@link Example}. In case no match could be
* found an empty {@link Iterable}
* is returned.
*
* @param example must not be {@literal null}.
* @return all entities matching the given {@link Example}.
*/
<S extends T> Iterable<S> findAll(Example<S> example);
/**
* Returns all entities matching the given {@link Example} applying the given {@link Sort}.
* In case no match could be
* found an empty {@link Iterable} is returned.
*
* @param example must not be {@literal null}.
* @param sort the {@link Sort} specification to sort the results by,
* may be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities matching the given {@link Example}.
* @since 1.10
*/
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
/**
* Returns a {@link Page} of entities matching the given {@link Example}.
* In case no match could be found, an empty
* {@link Page} is returned.
*
* @param example must not be {@literal null}.
* @param pageable the pageable to request a paged result, can be
* {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @return a {@link Page} of entities matching the given {@link Example}.
*/
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
/**
* Returns the number of instances matching the given {@link Example}.
*
* @param example the {@link Example} to count instances for. Must not be {@literal null}.
* @return the number of instances matching the {@link Example}.
*/
<S extends T> long count(Example<S> example);
/**
* Checks whether the data store contains elements that match the given {@link Example}.
*
* @param example the {@link Example} to use for the existence check.
* Must not be {@literal null}.
* @return {@literal true} if the data store contains elements that
* match the given {@link Example}.
*/
<S extends T> boolean exists(Example<S> example);
/**
* Returns entities matching the given {@link Example} applying the
* {@link Function queryFunction} that defines the
* query and its result type.
*
* @param example must not be {@literal null}.
* @param queryFunction the query function defining projection, sorting, and the result type
* @return all entities matching the given {@link Example}.
* @since 2.6
*/
<S extends T, R> R findBy
(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}
测试
public interface MyPersonRepository extends Repository<Person, Integer>,
QueryByExampleExecutor<Person> {
@Override
List<Person> findAll(Example example);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
//动态匹配的实体对象
var person=new Person();
person.setFirstName("ok3");
// 因为 id 有默认值,需要排除
var matcher= ExampleMatcher.matching().withIgnorePaths(Person_.ID);
//字符串 Starting 匹配,也能对所有的字符串属性指定字符串匹配
matcher= matcher.withMatcher(Person_.FIRST_NAME, ExampleMatcher.
GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING,true));
// 方法2 matcher= matcher.withMatcher(Person_.FIRST_NAME, p->p.endsWith().ignoreCase());
//用动态查询去匹配
var list=repository.findAll(Example.of(person,matcher));
list.forEach(p->{
System.out.println(p);
});
}
}
流畅的动态化 findBy
@Entity
@Getter
@Setter
@ToString
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "firstName", nullable = true)
String firstName;
@Column(name = "lastName", nullable = true)
String lastName;
@Column(name = "personAge", nullable = true)
int age;
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var person=new Person();
person.setFirstName("k1");
person.setLastName("dA1");
//数字类型的字段忽略掉,最好是用包装类型就不用在这里忽略了,因为 null 类型, Example B不会进行匹配
var exampleMatcher= ExampleMatcher.matching().
withIgnorePaths(Person_.ID).withIgnorePaths(Person_.AGE);
var sort= Sort.by(Person_.FIRST_NAME).ascending();
//可以在第二个参数里选取第一个数据
var result= repository.findBy(Example.of(person,exampleMatcher),p->p.sortBy(sort).first());
System.out.println(result.isPresent()?result.get():null);
}
}
JpaSpecificationExecutor-可组合的规约查询
规约的最大好处就是可组合可复用
hibernate 的使用方式
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Person> criteriaQuery = cb.createQuery(Person.class);
Root<Person> root = criteriaQuery.from(Person.class);
criteriaQuery.select(root).where(cb.equal(root.get("firstName"), "ok1"));
Query<Person> query = session.createQuery(criteriaQuery);
List<Person> results = query.getResultList();
Spring Data JPA 采用 Eric Evans 的书“领域驱动设计”中的规范概念,遵循相同的语义并提供 API 以使用 JPA 标准 API 定义此类规范。为了支持规范,您可以使用接口扩展您的存储库接口JpaSpecificationExecutor,如下所示:
public interface JpaSpecificationExecutor<T> {
/**
* Returns a single entity matching the given {@link Specification} or {@link Optional#empty()} if none found.
*
* @param spec can be {@literal null}.
* @return never {@literal null}.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found.
*/
Optional<T> findOne(@Nullable Specification<T> spec);
/**
* Returns all entities matching the given {@link Specification}.
*
* @param spec can be {@literal null}.
* @return never {@literal null}.
*/
List<T> findAll(@Nullable Specification<T> spec);
/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
*
* @param spec can be {@literal null}.
* @param pageable must not be {@literal null}.
* @return never {@literal null}.
*/
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
/**
* Returns all entities matching the given {@link Specification} and {@link Sort}.
*
* @param spec can be {@literal null}.
* @param sort must not be {@literal null}.
* @return never {@literal null}.
*/
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
/**
* Returns the number of instances that the given {@link Specification} will return.
*
* @param spec the {@link Specification} to count instances for. Can be {@literal null}.
* @return the number of instances.
*/
long count(@Nullable Specification<T> spec);
}
规约的接口
public interface Specification<T> extends Serializable {
long serialVersionUID = 1L;
/**
* Negates the given {@link Specification}.
*
* @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
* @param spec can be {@literal null}.
* @return guaranteed to be not {@literal null}.
* @since 2.0
*/
static <T> Specification<T> not(@Nullable Specification<T> spec) {
return spec == null //
? (root, query, builder) -> null //
: (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder));
}
/**
* Simple static factory method to add some syntactic sugar around a {@link Specification}.
*
* @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
* @param spec can be {@literal null}.
* @return guaranteed to be not {@literal null}.
* @since 2.0
*/
static <T> Specification<T> where(@Nullable Specification<T> spec) {
return spec == null ? (root, query, builder) -> null : spec;
}
/**
* ANDs the given {@link Specification} to the current one.
*
* @param other can be {@literal null}.
* @return The conjunction of the specifications
* @since 2.0
*/
default Specification<T> and(@Nullable Specification<T> other) {
return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
}
/**
* ORs the given specification to the current one.
*
* @param other can be {@literal null}.
* @return The disjunction of the specifications
* @since 2.0
*/
default Specification<T> or(@Nullable Specification<T> other) {
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
}
/**
* Creates a WHERE clause for a query of the referenced entity in form of a
* {@link Predicate} for the given
* {@link Root} and {@link CriteriaQuery}.
*
* @param root must not be {@literal null}.
* @param query must not be {@literal null}.
* @param criteriaBuilder must not be {@literal null}.
* @return a {@link Predicate}, may be {@literal null}.
*/
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
测试代码
为了在规约中使用强类型的属性名编程, JPA 提供了 metemodel 的概念在编译时生成代码, 为了使用此代码需要把生成的代码转变为源代码
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.6.3.Final</version>
</dependency>
//使用规约,继承 JpaSpecificationExecutor 接口
public interface MyPersonRepository extends Repository<Person, Integer>,
JpaSpecificationExecutor<Person> {
@Override
List<Person> findAll(Specification<Person> spec);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
//生成规约表达式,因为是函数式接口,此处也可以用 lambda 进行简写
var spec= new Specification<Person>() {
/**
*
* @param Root<Person> root 查询的实体
* @param CriteriaQuery<?> query 代表一个specific的顶层查询对象,它包含着查询的各个部分,
* 比如:select、from、where、group by、order by等。它提供了查询ROOT的方法
* @param criteriaBuilder 用来构建CritiaQuery的构建器对象,其实就相当于条件或者是条件组合,
* 即以Predicate的形式返回,可以构建简单的查询
复杂的查询还有要通过 CriteriaQuery<?> 生成
* @return
*/
@Override
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
//获取的实体属性值,此处用了 metemodel 强类型
var path1= root.get(Person_.firstName);
var path2= root.get(Person_.lastName);
//谓词判断
var predicate1 =criteriaBuilder.equal(path1,"ok1");
var predicate2 =criteriaBuilder.equal(path2,"ok2");
var and= criteriaBuilder.and(predicate1,predicate2);
//排序条件
var order1=criteriaBuilder.asc(path1);
var order2=criteriaBuilder.asc(path2);
//通过 query 组合
return query.where(and).orderBy(order1,order2).getRestriction();
//不带排序
//return criteriaBuilder.and(predicate1,predicate2);
}
};
var result = repository.findAll(spec);
System.out.println(result.size());
}
}
QueryDSL - 解决多表查询,动态查询的通用类型安全的查询利器,代替JPQL
QueryDSL 有多个模块,可以支持 JPA、Mongodb、Lucene 等,使用它们的 API 是统一的平台,这里我们用到的 module 是 JPA。
不可否认的是 JPA 使用是非常方便的,极简化的配置,只需要使用注解,无需任何 xml 的配置文件,语义简单易懂,但是,以上的一切都建立在单表查询的前提下的,我们可以使用 JPA 默认提供的方法,简单加轻松的完成 CRUD 操作。 但是如果涉及到多表动态查询, JPA 的功能就显得有些捉襟见肘了,虽然我们可以使用注解 @Query ,在这个注解中写 SQL 或者 HQL 都是在拼接字符串,并且拼接后的字符串可读性非常的差,当然 JPA 还为我们提供了 Specification 来做这件事情
QuerydslPredicateExecutor 接口提供的方法
public interface QuerydslPredicateExecutor<T> {
/**
* Returns a single entity matching the given {@link Predicate} or
* {@link Optional#empty()} if none was found.
*
* @param predicate must not be {@literal null}.
* @return a single entity matching the given {@link Predicate} or
* {@link Optional#empty()} if none was found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException
* if the predicate yields more than one
* result.
*/
Optional<T> findOne(Predicate predicate);
/**
* Returns all entities matching the given {@link Predicate}. In case
* no match could be found an empty
* {@link Iterable} is returned.
*
* @param predicate must not be {@literal null}.
* @return all entities matching the given {@link Predicate}.
*/
Iterable<T> findAll(Predicate predicate);
/**
* Returns all entities matching the given {@link Predicate} applying the
* given {@link Sort}. In case no match could
* be found an empty {@link Iterable} is returned.
*
* @param predicate must not be {@literal null}.
* @param sort the {@link Sort} specification to sort the results by,
* may be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities matching the given {@link Predicate}.
* @since 1.10
*/
Iterable<T> findAll(Predicate predicate, Sort sort);
/**
* Returns all entities matching the given {@link Predicate} applying
* the given {@link OrderSpecifier}s. In case no
* match could be found an empty {@link Iterable} is returned.
*
* @param predicate must not be {@literal null}.
* @param orders the {@link OrderSpecifier}s to sort the results by,
* must not be {@literal null}.
* @return all entities matching the given {@link Predicate} applying
* the given {@link OrderSpecifier}s.
*/
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
/**
* Returns all entities ordered by the given {@link OrderSpecifier}s.
*
* @param orders the {@link OrderSpecifier}s to sort the results by, must not be {@literal null}.
* @return all entities ordered by the given {@link OrderSpecifier}s.
*/
Iterable<T> findAll(OrderSpecifier<?>... orders);
/**
* Returns a {@link Page} of entities matching the given {@link Predicate}.
* In case no match could be found, an empty
* {@link Page} is returned.
*
* @param predicate must not be {@literal null}.
* @param pageable may be {@link Pageable#unpaged()}, must not be {@literal null}.
* @return a {@link Page} of entities matching the given {@link Predicate}.
*/
Page<T> findAll(Predicate predicate, Pageable pageable);
/**
* Returns the number of instances matching the given {@link Predicate}.
*
* @param predicate the {@link Predicate} to count instances for, must not be {@literal null}.
* @return the number of instances matching the {@link Predicate}.
*/
long count(Predicate predicate);
/**
* Checks whether the data store contains elements that match the given {@link Predicate}.
*
* @param predicate the {@link Predicate} to use for the existence check, must not
* be {@literal null}.
* @return {@literal true} if the data store contains elements that match the given
* {@link Predicate}.
*/
boolean exists(Predicate predicate);
/**
* Returns entities matching the given {@link Predicate} applying the {@link
* Function queryFunction} that defines the
* query and its result type.
*
* @param predicate must not be {@literal null}.
* @param queryFunction the query function defining projection, sorting, and the result type
* @return all entities matching the given {@link Predicate}.
* @since 2.6
*/
<S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>,
R> queryFunction);
}
准备工作
<!-- querydsl-jpa -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
此插件需要 querydsl-apt 依赖,此插件会在编译时生成强类型的实体类型,特征是生成在在实体前加Q的强类型元素据,类似于 JPA 的 metamodel,需要把生成的代码加到源码中
如下代码就是强类型的类型变量
QPerson person=QPerson.person;
//或
QPerson person= new QPerson("myPerson");
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
仓储继承 QuerydslPredicateExecutor 接口
QuerydslPredicateExecutor 接口是谓词查询,缺点是只能是单表查询, 返回的实体的所有字段
public interface MyPersonRepository extends Repository<Person, Integer>,
QuerydslPredicateExecutor<Person> {
@Override
List<Person> findAll(Predicate predicate);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var person=QPerson.person;
var booleanExpression1=person.firstName.in("k1", "k2");
var booleanExpression2=person.lastName.containsIgnoreCase("a1");
var booleanExpression3=person.id.goe(4);
var list= repository.findAll(booleanExpression1.and(booleanExpression2).and(booleanExpression3));
System.out.println(list.get(0).toString());
}
}
仓储注入 JPAQueryFactory 对象链式查询,多表,多字段查询
JPAQueryFactory 用来生成 JPAQuery,当然也可以自己 new JPAQuery
var myPerson = QPerson.person;
//可以自己构建 JPAQuery
JPAQuery<Person> query = new JPAQuery<Person>(entityManager);
//或
JPAQuery<Person> query = jPAQueryFactory.selectFrom(myPerson)
配置 JPAQueryFactory 工厂在仓储里使用
@Configuration
public class QueryDSLConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public JPAQueryFactory getJPAQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
链式写法的 jpaQuery 用java 语法写SQL 非常的方便
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> findDSL(JPAQueryFactory jPAQueryFactory){
var person=QPerson.person;
var jpaQuery=jPAQueryFactory.select(person)
.from(person)
.where(person.firstName.containsIgnoreCase("ok"));
var list= jpaQuery.fetch();
return list;
}
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var jPQLQueryFactory= context.getBean(JPAQueryFactory.class);
//调用存储方法
System.out.println(repository.findDSL(jPQLQueryFactory));
}
}
更新删除
public interface MyPersonRepository extends Repository<Person, Integer> {
@Transactional
default long updateDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var updateClause = jPAQueryFactory.update(person)
.set(person.firstName, "name good")
.where(person.id.eq(1));
return updateClause.execute();
}
@Transactional
default long deleteDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var deleteClause = jPAQueryFactory.delete(person)
.where(person.firstName.eq("K2"));
return deleteClause.execute();
}
}
查询部分字段
返回一个字段
public interface MyPersonRepository extends Repository<Person, Integer> {
default String queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
//选择映射的字段
var jpaQuery = jPAQueryFactory.select(person.lastName)
.from(person)
.where(person.id.eq(1));
return jpaQuery.fetchOne();
}
}
返回多个字段组合的是元组
public interface MyPersonRepository extends Repository<Person, Integer> {
default Tuple queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var jpaQuery = jPAQueryFactory.select(person.lastName,person.lastName)
.from(person)
.where(person.id.eq(1));
var tuple=jpaQuery.fetchOne();
System.out.println(tuple.get(person.lastName));
System.out.println(tuple.get(person.lastName));
return tuple;
}
}
返回多个字段组合的的元组不方便使用,还可以进行投影
//投影的类
@Value
public class MyReturnDto {
String firstNameDto;
String lastNameDto;
public String getFullName(){
return firstNameDto+","+lastNameDto;
}
}
public interface MyPersonRepository extends Repository<Person, Integer> {
default MyReturnDto queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var jpaQuery = jPAQueryFactory.select(
//对返回结果进行投影
Projections.constructor(MyReturnDto.class,
person.firstName.as("firstNameDto"),person.lastName.as("lastNameDto")))
.from(person)
.where(person.id.eq(1));
var myReturnDto=jpaQuery.fetchOne();
System.out.println(myReturnDto.getFullName());
return myReturnDto;
}
}
关联和子查询
关联查询,支持 内联结、联结、左联结和右联结
//增加的实体
@Entity
@Data
public class OrderCargo {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int PersonId;
private String CargoName;
}
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Tuple> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var orderCargo = QOrderCargo.orderCargo;
var jpaQuery= jPAQueryFactory.select(person,orderCargo).
from(person).join(orderCargo).on(person.id.eq(orderCargo.PersonId));
var result =jpaQuery.fetch();
//显示结果
result.forEach(p->{
var i= p.get(person);
var j=p.get(orderCargo);
System.out.println(i);
System.out.println(j);
});
return result ;
}
}
子查询,提过静态方法 JPAExpressions 提供子查询
//增加实体
@Entity
@Data
public class CargoType {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String CargoName;
}
通过子查询实现 orderCargo 的 CargoName 出现在 cargoType 的 CargoName 里
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<OrderCargo> queryDSL(JPAQueryFactory jPAQueryFactory) {
var orderCargo = QOrderCargo.orderCargo;
var type= QCargoType.cargoType;
var jpaQuery= jPAQueryFactory.selectFrom(orderCargo)
.where(orderCargo.CargoName.in(JPAExpressions.select(type.CargoName).from(type)));
var result= jpaQuery.fetch();
result.forEach(p->{
System.out.println(p);
});
return result ;
}
}
@Entity
@Getter
@Setter
@ToString
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "firstName", nullable = true)
String firstName;
@Column(name = "lastName", nullable = true)
String lastName;
@Column(name = "personAge", nullable = true)
int age;
}
//获取 person 里年龄大于平均年龄的集合
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var jpaQuery= jPAQueryFactory.selectFrom(person)
.where(person.age.goe(JPAExpressions.select(person.age.avg()).from(person)));
var result=jpaQuery.fetch();
result.forEach(p->{
System.out.println(p);
});
return result ;
}
}
// 或者
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var q=new QPerson("q");
var jpaQuery= jPAQueryFactory.selectFrom(person)
.where(person.age.goe(JPAExpressions.select(q.age.avg()).from(q)));
var result=jpaQuery.fetch();
result.forEach(p->{
System.out.println(p);
});
return result ;
}
}
分组和分页及排序
按照某个属性进行分组,获取也只能获取分组的属性,可以多次分组
//单个属性的分组
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Integer> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var list = jPAQueryFactory.select(person.age).from(person).groupBy(person.age).fetch();
list.forEach(p->{
System.out.println(p);
});
return list ;
}
}
//可以对多个元素的组合进行分组 groupBy(person.firstName,person.lastName)
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Object> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var list = jPAQueryFactory.select(person.firstName,person.lastName).from(person).
groupBy(person.firstName,person.lastName).fetch();
list.forEach(p->{
System.out.println(p);
});
return Collections.singletonList(list);
}
}
//分组过滤 having
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Integer> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var list=jPAQueryFactory.select(person.age).from(person)
//对分组的元素进行过滤 having(person.age.gt(20))
.groupBy(person.age).having(person.age.gt(20)).fetch();
list.forEach(p->{
System.out.println(p);
});
return list;
}
}
分页 通过 offset 设置页码,limit 设置每页记录数
public interface MyPersonRepository extends Repository<Person, Integer> {
default QueryResults<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var pageable= PageRequest.of(0,2);
var queryResults=jPAQueryFactory.selectFrom(person)
.offset(pageable.getOffset()).limit(pageable.getPageSize()).fetchResults();
System.out.println(queryResults.getTotal());
queryResults.getResults().forEach(p->{
System.out.println(p);
});
return queryResults;
}
}
排序 orderBy
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var list=jPAQueryFactory.selectFrom(person)
//多个排序条件 asc 升序 ,
//或 orderBy(person.age.asc(),person.id.asc())
.orderBy(person.age.asc()).orderBy(person.id.asc()).fetch();
return list;
}
}
聚合函数,本质还是调用数据库的聚合函数
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<String> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var max=jPAQueryFactory.select(person.age.max()).from(person).fetchOne();
System.out.println(max);
var min=jPAQueryFactory.select(person.age.min()).from(person).fetchOne();
System.out.println(min);
var avg=jPAQueryFactory.select(person.age.avg()).from(person).fetchOne();
System.out.println(avg);
var sum=jPAQueryFactory.select(person.age.sum()).from(person).fetchOne();
System.out.println(sum);
//数字的减法,需要的有加法等
var list=jPAQueryFactory.select(person.age.subtract(1)).from(person).fetch();
list.forEach(p->{
System.out.println(p);
});
//字符可以插入前缀或后缀
var list1=jPAQueryFactory.select(person.firstName.prepend("hi,")).from(person).fetch();
list1.forEach(p->{
System.out.println(p);
});
return list1;
}
}
spring 扩展的 参数分页和排序-Pageable/Sort
Pageable 分页
public interface MyPersonRepository extends Repository<Person, Integer> {
Page<Person> findPersonByName(String name, Pageable pageable);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
Pageable pageable= PageRequest.of(0,2 );
var pages= repository.findPersonByName("ok", pageable);
System.out.println("可查询的总总页数"+pages.getTotalPages());
System.out.println("可查询的总条目数"+pages.getTotalElements());
System.out.println("本次查询到的集合"+pages.get().collect(Collectors.toList()));
}
}
Sort 排序
推荐 Sort.TypedSort<> 排序,而不是硬编码的排序
public interface MyPersonRepository extends Repository<Person, Integer> {
List
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
//硬编码的排序
Sort sort=Sort.by("name").ascending();
//强类型引用的排序
Sort.TypedSort<Person> sortPerson=Sort.sort(Person.class);
sortPerson.by(Person::getName).ascending();
var list= repository.findPersonByName("ok", sortPerson);
System.out.println(list);
}
}
分页和排序一起使用
public interface MyPersonRepository extends Repository<Person, Integer> {
Page<Person> findPersonByName(String name, Pageable pageable);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
Sort.TypedSort<Person> sortPerson=Sort.sort(Person.class);
sortPerson.by(Person::getName).ascending();
//分页和排序
Pageable pageable= PageRequest.of(0,2 ,sortPerson);
var pages= repository.findPersonByName("ok", pageable);
System.out.println("可查询的总总页数"+pages.getTotalPages());
System.out.println("可查询的总条目数"+pages.getTotalElements());
System.out.println("本次查询到的集合"+pages.get().collect(Collectors.toList()));
}
}
查询返回结果
常见的返回结果:
- List
- Slice
- Page
- Stream
- CompletableFuture
- Optional
Stream 流的传输需要数据库的配合,数据库要支持数据的流式传输才行,需要在消费流的方法上加 @Transactional(readOnly = true),流在用完后记得要关闭,这种方式的好处是对数据量大处理比较好
@Component
class Bar{
private final MyPersonRepository myPersonRepository;
public Bar(MyPersonRepository myPersonRepository){
this.myPersonRepository=myPersonRepository;
}
@Transactional(readOnly = true)
public void f(){
//自动关闭流
try(var stream= myPersonRepository.findPersonByName("ok")) {
stream.forEach(p-> System.out.println(p.getId()));
}
}
}
CompletableFuture 可以使用Spring的异步方法执行功能异步的存储库查询。这意味着方法将在调用时立即返回,并且实际的查询执行将发生在已提交给Spring TaskExecutor的任务中,比较适合定时任务的实际场景
public interface MyPersonRepository extends Repository<Person, Integer> {
CompletableFuture<List<Person>> findPersonByName(String name);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository=context.getBean(MyPersonRepository.class);
var future=repository.findPersonByName("ok");
future.thenAcceptAsync(p->{
System.out.println(p.stream().collect(Collectors.toList()));
});
System.out.println("结束");
}
}
Page 继承自 Slice ,Slice 不返回分页的可以使用的总页数,关心的是是否有下一页
public interface MyPersonRepository extends Repository<Person, Integer> {
Slice<Person> findPersonByFirstName(String name,Pageable pageable);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var pageable = PageRequest.of(0, 2);
var slice = repository.findPersonByFirstName("ok", pageable);
System.out.println(slice.getContent());
if(slice.hasNext()){
System.out.println("还有数据可以加载");
}
}
}
使用 Optional 避免空指针
如下代码会报错 ❌
public interface MyPersonRepository extends Repository<Person, Integer> {
Person findTop1ByFirstName(String name);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
//会报空指针异常
var person = repository.findTop1ByFirstName("not exist");
System.out.println(person);
}
}
正确的方法 Optional ✅
public interface MyPersonRepository extends Repository<Person, Integer> {
Optional<Person> findTop1ByFirstName(String name);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var optionalPerson = repository.findTop1ByFirstName("not exist");
optionalPerson.ifPresent(p->{
System.out.println(p);
});
}
}
Projections(投影)对查询结果的扩展
基于接口的投影
使用方法定义查询是不能返回部分属性的,通过 Projections 可以解决 如下代码查询数据库的时候只会查询一个属性
@Entity
@Getter
@Setter
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "firstName", nullable = true)
String firstName;
@Column(name = "lastName", nullable = true)
String lastName;
}
//只需要查询一个属性,接口里要写获取属性的方法
public interface MyReturn {
String getFirstName();
}
public interface MyPersonRepository extends Repository<Person, Integer> {
MyReturn findFirstByFirstName(String name);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var result = repository.findFirstByFirstName("ok1");
System.out.println(result.getFirstName());
}
}
如下方法对返回的结果在一个方法里进行投影
public interface MyReturn {
@Value("#{target.firstName+','+target.lastName}")
String getFullName();
}
//或者
@Component("myBean")
public class MyBean {
public String getFullName(Person person){
//这里可以写复杂的语句用于生成结果
return person.getFirstName()+","+person.getLastName();
}
}
public interface MyReturn {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
}
基于DTO 类的投影-推荐
public class MyReturnDto {
private final String firstName;
private final String lastName;
public MyReturnDto(String firstName, String lastName){
this.firstName=firstName;
this.lastName=lastName;
}
public String getFullName(){
return firstName+","+lastName;
}
}
//@Value lombok 简化
@Value
public class MyReturnDto {
private String firstName;
private String lastName;
public String getFullName(){
return firstName+","+lastName;
}
}
public interface MyPersonRepository extends Repository<Person, Integer> {
MyReturnDto findFirstByFirstName(String name);
}
jpa-hibernate 实体的继承关系
会使得表与实体之间的关系变得复杂不直观,增加复杂度,不建议使用。
@MappedSuperclass 只能用在类上
基于代码复用和模型分离的思想,在项目开发中使用JPA的@MappedSuperclass注解将实体类的多个属性分别封装到不同的非实体类中,纯粹的继承,和表没关系,对象之间的字段共享 注意:
- 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
- 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。
此表在数据库不会存在
@Setter
@Getter
@MappedSuperclass
public abstract class Audit {
@Column(name = "createUser")
private String createBy;
@Column(name = "editUser")
private String editBy;
}
/*
继承 @MappedSuperclass 标注的类
*/
@Entity
@Data
public class Bar extends Audit {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;
}
@SecondaryTable 数据库把主表的部分字段分到子表 -不用,增加复杂度
子表1
@Setter
@Getter
@Entity
public class Base1 {
@Id
private Long id;
public String name1;
}
子表2
@Setter
@Getter
@Entity
public class Base2 {
@Id
private Long id;
public String name2;
}
主表 @SecondaryTable(name ="base1",pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")) 指定主表的相关字段数据保存在子表 在数据库里主表自有 id ,name 字段,其他字段都是保存在子表里
@Entity
@Setter
@Getter
//定义子表的路径在哪里
@SecondaryTable(name ="base1",pkJoinColumns = @PrimaryKeyJoinColumn(name = "id"))
@SecondaryTable(name ="base2",pkJoinColumns = @PrimaryKeyJoinColumn(name = "id"))
public class Bar {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
//告诉 jpa 此字段的数据保存在那个子表
@Column(table = "base1")
public String name1;
//告诉 jpa 此字段的数据保存在那个子表
@Column(table = "base2")
public String name2;
}
@Inheritance -不用,增加复杂度
- @Inheritance (strategy = InheritanceType.SINGLE_TABLE) 将所有父类和子类集合在一张表,只有父类有 @Table ,其他表的字段集合到父类 ,在父类里加字段进行区分子类
- @Inheritance (strategy = InheritanceType.TABLE_PER_CLASS) 每个子类会生成一张单独的表,父类可以查询所有子类的表数据
- @Inheritance (strategy = InheritanceType.JOINED) 每个类分别生成一张单独的表,但是每张表只有自己的属性,没有父类的属性,通过外键关联的形式使两张表关联起来
jpa-hibernate @Entity Callbacks-的回调方法
JPA 协议里面规定,可以通过一些注解,为其监听回调事件、指定回调方法
注解 | 备注 |
---|---|
@PrePersist | 在新增之前的回调 |
@PostPersist | 在新增到数据库之前的回调 |
@PreRemove | 在数据库删除之前的回调 |
@PostRemove | 在数据库删除成功之后的回调 |
@PreUpdate | 在更新到数据库之前回调 |
@PostUpdate | 在数据库更新之后的回调 |
@PostLoad | 在实体加载之后的回调 |
语法注意事项,关于上表所述的几个方法有一些需要注意的地方,如下:
-
回调函数都是和 EntityManager.flush 或 EntityManager.commit 在同一个线程里面执行的,只不过调用方法有先后之分,都是同步调用,所以当任何一个回调方法里面发生异常,都会触发事务进行回滚,而不会触发事务提交。
-
Callbacks 注解可以放在实体里面,可以放在 super-class 里面,也可以定义在 entity 的 listener 里面,但需要注意的是:放在实体(或者 super-class)里面的方法,签名格式为“void ()”,即没有参数,方法里面操作的是 this 对象自己;放在实体的 EntityListener 里面的方法签名格式为“void (Object)”,也就是方法可以有参数,参数是代表用来接收回调方法的实体。
-
使上述注解生效的回调方法可以是 public、private、protected、friendly 类型的,但是不能是 static 和 final 类型的方法。
在标注 @super-class 注解的实体里写回调方法
在 @MappedSuperclass 标记的类里,声明的回调会在所有的实现类里起到横向拦截的功能,可以自己实现审计追踪的功能
@Setter
@Getter
@MappedSuperclass
public abstract class Audit {
@Column(name = "createUser")
private String createBy;
@Column(name = "editUser")
private String editBy;
@PrePersist
public void prePersist(){
//在新增数据未到数据库前,修改的数据会到数据库
this.setCreateBy( this.getCreateBy()+"新增前");
System.out.println(this+",prePersist");
}
@PostPersist
public void postPersist(){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
this.setCreateBy( this.getCreateBy()+"新增后");
System.out.println(this+",postPersist");
}
@PreUpdate
public void preUpdate(){
//在新增数据未到数据库前,修改的数据会到数据库
this.setCreateBy( this.getCreateBy()+"更新前");
System.out.println(this+",preUpdate");
}
@PostUpdate
public void postUpdate(){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
this.setCreateBy( this.getCreateBy()+"更新后");
System.out.println(this+",postUpdate");
}
@PreRemove
public void preRemove(){
System.out.println(this+",preRemove");
}
@PostRemove
public void postRemove(){
System.out.println(this+",postRemove");
}
@PostLoad
public void load(){
System.out.println(this+",load");
}
}
@Entity
@Data
public class Bar extends Audit {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;
@Override
public String toString(){
return this.getCreateBy()+";"+this.getEditBy();
}
}
@Entity
@Data
public class Foo extends Audit {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;
@Override
public String toString(){
return this.getCreateBy()+";"+this.getEditBy();
}
}
自定义 EntityListener,不建议使用
此类实例化一次
public class MyAuditingEntityListener {
@PrePersist
public void prePersist(Audit obj){
//在新增数据未到数据库前,修改的数据会到数据库
obj.setCreateBy( obj.getCreateBy()+"新增前");
System.out.println(obj+",prePersist");
}
@PostPersist
public void postPersist(Audit obj){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
obj.setCreateBy( obj.getCreateBy()+"新增后");
System.out.println(obj+",postPersist");
}
@PreUpdate
public void preUpdate(Audit obj){
//在新增数据未到数据库前,修改的数据会到数据库
obj.setCreateBy( obj.getCreateBy()+"更新前");
System.out.println(obj+",preUpdate");
}
@PostUpdate
public void postUpdate(Audit obj){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
obj.setCreateBy( obj.getCreateBy()+"更新后");
System.out.println(obj+",postUpdate");
}
@PreRemove
public void preRemove(Audit obj){
System.out.println(obj+",preRemove");
}
@PostRemove
public void postRemove(Audit obj){
System.out.println(obj+",postRemove");
}
@PostLoad
public void load(Audit obj){
System.out.println(obj+",load");
}
}
通过 @EntityListeners 和回调类进行绑定
@Setter
@Getter
@MappedSuperclass
@EntityListeners(MyAuditingEntityListener.class)
public abstract class Audit {
@Column(name = "createUser")
private String createBy;
@Column(name = "editUser")
private String editBy;
}
jpa-hibernate 乐观锁和重试机制
@Version 原理是会在一次查询时获得 version ,做更新后,在更新到数据库前再次到数据库查询最新的 version ,对比这俩个 version 的数值是否相等,如果相等说明在此时间内数据库的此条记录没有更新,本次操作事务可以成功,否则事务执行失败抛出错误
注意点如果是通过 @Query 执行JPQL , version 需要自己控制
@Modifying
@Query("update Account set name=:name, version=:version+1 where id=:id and version=:version")
int updateBarByVersion(@Param("id") int id,@Param("name") String name, @Param("version") int version);
//service中加判断,抛异常,这样就自己通过数据库实现了乐观锁
@Transactional(rollbackFor = Exception.class)
public int updateBarService(Bar bar){
int i =Dao.updateBarByVersion(bar.getId(),bar.getName(),bar.getVersion());
if(i==0){
throw new ObjectOptimisticLockingFailureException("更新bar失败",new Exception());
}
return i;
}
通过JPA 仓库 实现 CRUD , version 字段不要自己控制 ,jpa 会做判断
@Entity
@Data
public class Bar {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;
private String name;
//此字段有 JPA 自己进行更新,所以设置 AccessLevel.PRIVATE
@Version
@Setter(AccessLevel.PRIVATE)
private int version;
}
如果因为 version 不一致就让事务失败了,就放弃此次操作,是不合理的(如网络问题),加入重试机制解决
@EnableRetry 让重试起作用 , @Retryable 在需要重试方法或类上加此注解
@Retryable
- value:指定发生的异常进行重试
- include:和value一样,默认空,当exclude也为空时,所有异常都重试
- exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试
- maxAttemps:重试次数,默认3
- backoff:重试补偿机制,默认没有
@Backoff注解
- delay:指定延迟后重试
- multiplier:指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二次为10秒,第三次为20秒
@Recover
- 当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。
!参考(https://www.baeldung.com/spring-retry,https://github.com/spring-projects/spring-retry)
依赖的包
<!-- 使用 spring-retry 包实现重试 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 重试的机制是 aop 代理,需要引入此包 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.9.1</version>
</dependency>
public interface MyRepository extends JpaRepository<Bar, Long> {
}
/*
定义基类处理重试
*/
@Retryable(value = RuntimeException.class, maxAttempts = 3, backoff =
@Backoff(value = 1000, multiplier = 1.5))
public abstract class BaseService {
/**
* 被此注解处理的异常,不会向上抛出,除非手动抛出异常
* @param exception
*/
@Recover
public void fun(RuntimeException exception){
System.out.println("连续重试还是无效:"+exception.getMessage());
}
}
/*
服务类
*/
@Component
public class MyService extends BaseService{
@Autowired
private MyRepository myRepository;
public void f(){
var result= myRepository.findById(58L);
result.ifPresent(p->{
p.setName(LocalDateTime.now().toString());
myRepository.save(p);
});
}
}
启动类上加 @EnableRetry 注解
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableRetry
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var myService= context.getBean(MyService.class);
myService.f();
}
}
jpa-hibernate 提供的注解
注解 | 说明 |
---|---|
@Entity | 将此类标记为实体,被标注的实体,将映射到数据库 |
@Embedded | 将此类标记为值对象 |
@Table | 指定此实体的数据的表名,不指定默认用类名 |
@Id | 将此类的字段标记为主键 |
@IdClass | 利用外部类定义联合主键,标记在实体类上 |
@Column | 将此类的字段和数据库的列名进行绑定 |
@Version | 用于支持乐观锁版本控制 |
@Transient | 标注的字段不会在数据库中存在,比如年龄可以自动计算 |
@Enumerated | 如果字段是枚举类型,默认映射到数据库的是 int 类型,如果想要映射字符串 @Enumerated(EnumType.STRING),建议映射字符串 |
@GeneratedValue | 为主键生成策略 |
@Basic | 表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为@Basic |
@Temporal | 旧的 java.util.Date 不能区分日期和时间,但是数据库默认有 date , time , datetime. 此注解就是指定映射关系 |
@Lob | 将属性映射为数据库支持的大数据对象类型 |
@Access | 用于指定 entity 里面的注解是写在字段上面,还是 get/set 方法上面生效,非必填。在默认不填写的情况下,当实体里面的第一个注解出现在字段上或者 get/set 方法上面,就以第一次出现的方式为准,不能一些写在字段上,一些写在方法上 |
@Table
@Target(TYPE)
@Retention(RUNTIME)
public @interface Table {
/**
* 数据库里的表名
*/
String name() default "";
/**
* 数据库实例名,在驱动连接的 url 里已经指定了实例,默认不指定数据库的表出现在 url 里的实例里
* 如果在这里指定实例,这此表会在此实例里创建,如果数据库根本就没有此实例,者创建表的过程失败
*/
String catalog() default "";
/**
* 不是所有数据库都支持。mySQL 就不支持
*/
String schema() default "";
/**
* 指定此表的唯一约束
@Table(name = "users", uniqueConstraints = {
//复合字段的唯一约束
@UniqueConstraint(columnNames = {"name", "age"}),
//单个字段的唯一约束
@UniqueConstraint(columnNames = {"account"})
})
*/
UniqueConstraint[] uniqueConstraints() default {};
/**
指定此表的索引
*/
Index[] indexes() default {};
}
@IdClass , @EmbeddedId 联合主键
@Embeddable 与 @EmbeddedId 注解同样可以做到联合主键的效果,并且更方便
作为符合主键类,要满足以下几点要求。
- 必须实现Serializable接口。
- 必须有默认的public无参数的构造方法。
- 必须覆盖equals和hashCode方法。
- equals方法用于判断两个对象是否相同,EntityManger通过find方法来查找Entity时是根据equals的返回值来判断的。
- hashCode方法返回当前对象的哈希码,用于equals判断
联合主键必须定义在一个类里
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CityIdentity implements Serializable {
private String name;
private String address;
}
@IdClass 定义联合主键,缺点是要写字符串的硬编码
@Entity
@Data
//告知 jpa 此实体的的联合组建是 CityIdentity
@IdClass(CityIdentity.class)
public class City {
@Id
@Column(name="name")
//属性名必须是在 CityIdentity 里定义的属性
private String name;
@Id
@Column(name="address")
//属性名必须是在 CityIdentity 里定义的属性
private String address;
private int peopleAccount;
}
public interface MyPersonRepository extends CrudRepository<City, CityIdentity> {
List<City> findCityByPeopleAccount(int account);
}
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
{
var city=new City();
city.setName("bj");
city.setAddress("维度8");
city.setPeopleAccount(10000);
repository.save(city);
}
{
var city=new City();
city.setName("sh");
city.setAddress("维度8");
city.setPeopleAccount(10000);
repository.save(city);
}
{
var id=new CityIdentity();
id.setAddress("维度8");
id.setName("bj");
//通过ID 查询
var city = repository.findById(id);
System.out.println(city);
}
}
}
@EmbeddedId 定义联合主键,优点是不需要要写字符串的硬编码
@Data
@NoArgsConstructor
@AllArgsConstructor
//把联合主键定义成嵌入类型
@Embeddable
public class CityIdentity implements Serializable {
private static final long serialVersionUID = -8229477451589838531L;
private String name;
private String address;
}
@Entity
@Data
public class City {
//嵌入式的主键
@EmbeddedId
private CityIdentity cityIdentity;
private int peopleAccount;
}
@GeneratedValue
public enum GenerationType {
/*
使用一个特定的数据库表格来保存主键,持久化引擎通过关系数据库的一张特定的表格来生成主键,
这种策略的好处就是不依赖于外部环境和数据库的具体实现,在不同数据库间可以很容易的进行移植
*/
TABLE,
/*
在某些数据库中,不支持主键自增长,比如Oracle,其提供了一种叫做"序列(sequence)"的机制生成主键
*/
SEQUENCE,
/*
数据库生成自增长的 id
*/
IDENTITY,
/*
默认值,有 jip 根据数据库引擎自己决定
*/
AUTO
}
@Column
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Column {
/**
数据库内映射的列名
*/
String name() default "";
/**
此字段是否唯一,也可以在 @Table 里指定唯一约束
*/
boolean unique() default false;
/**
* 是否可为 null
*/
boolean nullable() default true;
/**
* 执行 insert 时是否包含此字段
*/
boolean insertable() default true;
/**
* 执行 update 时是否包含此字段
*/
boolean updatable() default true;
/**
* 此字段在数据库中的指定的数据类型
* 编程语言中字符串一般都用String表示,但是数据库中varcahr数值类型有长度限制,
* 一旦需要大文本,则需要text数值类型
* 但是String类型默认映射的数值类型是varchar,columnDefinition可以进行额外指定
* -> columnDefinition="text"
*/
String columnDefinition() default "";
/**
* todo 不确定做什么用
*/
String table() default "";
/**
* 如果是字符串类型 ,可以指定字符串的长度
*/
int length() default 255;
/**
* 表示浮点数的总长度(整数部分和小数部分的长度和)
*/
int precision() default 0;
/**
* 表示浮点数的小数部分的长度
*/
int scale() default 0;
}
@Temporal
public enum TemporalType {
/** Map as <code>java.sql.Date</code> */
DATE,
/** Map as <code>java.sql.Time</code> */
TIME,
/** Map as <code>java.sql.Timestamp</code> */
TIMESTAMP
}
@Entity
@Data
@IdClass(CityIdentity.class)
public class City {
@Id
@Column(name="name")
private String name;
@Id
@Column(name="address")
private String address;
private int peopleAccount;
//为了配合旧的 Date API
@Temporal(TemporalType.DATE)
private Date overDate;
//如下采用新的 API 根本不需要 @Temporal 做映射
private LocalDate createDate;
private LocalTime updateTime;
private LocalDateTime happenDateTime;
}
@Lob
@Lob 注解支持以下数据库类型的字段:
- Clob( Character Large Ojects) 类型是长字符串类型,java.sql.Clob、Character[]、 char[] 和 String 将被映射为 Clob 类型。
- Blob( Binary Large Objects) 类型是字节类型,java.sql.Blob、Byte[]、byte[] 和实现了 Serializable 接口的类型将被映射为 Blob 类型。
注意:Clob、 Blob 占用内存空间较大,一般配合 @Basic(fetch=FetchType.LAZY) 注解将其设置为延迟加载。
//可序列的对象也可以保存为数据库的二进制数据
@Data
public class EmbObject implements Serializable {
private String name;
private int account;
}
@Entity
@Data
public class BlobObject {
@Id
@Column(name = "id", nullable = false)
private Long id;
//longblob
@Lob
private String summary1;
//longblob
@Lob
private char[] summary2;
//longblob
@Lob
private Character[] summary3;
//longtext
@Lob
private byte[] face1;
//longtext
@Lob
private Byte[] face2;
//longtext
@Lob
private EmbObject embObject;
}
@Basic
//TODO 对非关联对象的字段使用延迟加载没有效果 某些情况下不需要立即加载某字段的值,可以采用延迟加载
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Basic {
/*
LAZY 延迟加载, 立即加载 EAGER,默认立即加载此字段
*/
FetchType fetch() default EAGER;
boolean optional() default true;
}
@Embeddable , @Embedded 值对象
注意点:
- @Embeddable 定义没有主键的值对象
- @Embedded 把值对象嵌入实体类里面
- @AttributeOverrides 如果一个实体类有多个相同的值对象,用此注解去定义不同值对象在数据库的列名 麻烦之处是如果值对象的字段比较多,此注解写起来相当麻烦
定义值对象
@Data
@Embeddable
public class MyAddress {
private String road;
private String city;
}
嵌入值对象到实体对象
@Entity
@Data
public class CargoOrder {
@Id
private Long id;
private String cargoName;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "road",column = @Column(name = "sendAddress_road")),
@AttributeOverride(name = "city",column = @Column(name = "sendAddress_city"))
})
private MyAddress sendAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "road",column = @Column(name = "receiveAddress_road")),
@AttributeOverride(name = "city",column = @Column(name = "receiveAddress_city"))
})
private MyAddress receiveAddress;
}
嵌入值对象集合到实体对象
会另外生产一个表用于保存值对象的集合,数据库值对象集合的表和实体表是强关联的表
@Entity
@Data
public class CargoOrder {
@Id
@GeneratedValue
private Long id;
private String cargoName;
/*
@ElementCollection(fetch = FetchType.EAGER) 设置成立即加载对集合值对象,用默认的懒加载会报错
*/
@ElementCollection(fetch = FetchType.EAGER)
private List<MyAddress> receiveAddress;
}
针对集合也的写一个强类型的转换器
// 此注解会被 spring jpa 全局使用,不需要在值对象字段上加 @Convert(converter =
//MyInstanceJpaConvertJson.class)
// 如果不加此注解,需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// @Converter(autoApply = true)
@Converter
public class MyInstanceJpaConvertJson1 implements JpaConverterJson<List<Contacts>> {
@Override
public List<Contacts> getInstance() {
return new ArrayList<Contacts>() ;
}
}
值对象的其他解决方法
值对象作为可序列化对象
@Data
public class Contacts implements Serializable {
private String name;
private String phone;
}
序列化为二进制数据,对集合不行
@Entity
@Data
public class CargoOrder {
@Id
private Long id;
private String cargoName;
//默认不加注解,保存到数据库是二进制数据
@Column(name = "sendContacts")
private Contacts sendContacts;
}
自定义保存方式,这个最为灵活
自定义一个数据库和实体字段转换器,此转换器把对象转换为 json 字符串
定义一个泛型的转换器
public interface JpaConverterJson<T> extends AttributeConverter<T, String> {
T getInstance();
final static ObjectMapper objectMapper = new ObjectMapper();
@Override
default public String convertToDatabaseColumn(T meta) {
try {
return objectMapper.writeValueAsString(meta);
} catch (JsonProcessingException ex) {
return null;
// or throw an error
}
}
@Override
default public T convertToEntityAttribute(String dbData) {
try {
var obj = objectMapper.readValue(dbData, (Class<T>) getInstance().getClass());
return obj;
} catch (IOException ex) {
return null;
}
}
}
强类型的转换器,每个值对象都要定义一个
// @Converter(autoApply = true) 此注解会被 spring jpa 全局使用,不需要在值对象字段上加
@Convert(converter = MyInstanceJpaConvertJson.class)
// @Converter注解,需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
public class MyInstanceJpaConvertJson implements JpaConverterJson<Contacts> {
@Override
public Contacts getInstance() {
return new Contacts();
}
}
对实体里的值对象字段应用转换器
@Entity
@Data
public class CargoOrder {
@Id
private Long id;
private String cargoName;
@Column(name = "receiveContacts")
@Convert(converter = MyInstanceJpaConvertJson.class)
private Contacts receiveContacts;
}
嵌入值对象集合到实体对象
@Entity
@Data
public class CargoOrder {
@Id
@GeneratedValue
private Long id;
private String cargoName;
/**
* 这种方式可以写入但是不能读取,等于不能用
*/
//@Column(name = "sendContacts",columnDefinition = "blob")
//private List<Contacts> sendContacts=new ArrayList<>();
/*
自定义序列化方式是比较好用的
*/
@Column(name = "receiveContacts",length = 2000)
@Convert(converter = MyInstanceJpaConvertJson1.class)
private List<Contacts> receiveContacts;
}
针对集合也的写一个强类型的转换器
// 此注解会被 spring jpa 全局使用,不需要在值对象字段上加
@Convert(converter = MyInstanceJpaConvertJson.class)
// 如果不加此注解,需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// @Converter(autoApply = true)
@Converter
public class MyInstanceJpaConvertJson1 implements JpaConverterJson<List<Contacts>> {
@Override
public List<Contacts> getInstance() {
return new ArrayList<Contacts>() ;
}
}
jpa-hibernate 关联关系
spring data jpa 没有提供关联关系的扩展,关联关系注解包括 @JoinColumn、 @OneToOne、 @OneToMany、 @ManyToOne、 @ManyToMany、@JoinTable、 @OrderBy。 注意点: 多对多关联关系的实体需要加 Serializable 接口
@JoinColumn 注解用来定义主键字段和外键字段的对应关系
如果没有 @JoinColumn 注解的话默认通过两个实体的主键进行关联 name
@Repeatable(JoinColumns.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface JoinColumn {
/**
定义在数据库里的外键名,此外键名会在使用 @JoinColumn 注解的表里创建.主表通过外键名去查找关联表
默认不指定的化,值是关联表的表名+主键
*/
String name() default "";
/**
此字段用途是 name() 方法定义的外键,指向关联表的字段,默认不写的化是指向关联表的 id
name() 定义的字段里存放的数值是 referencedColumnName() 指定的字段
*/
String referencedColumnName() default "";
/**
外键字段是否唯一
*/
boolean unique() default false;
/**
外键字段是是否允许为空
*/
boolean nullable() default true;
/**
是否允许插入, false :关联表会插入数据,但是外键字段会为 null
*/
boolean insertable() default true;
/**
是否允许更新 没有测试过效果
*/
boolean updatable() default true;
/**
定义建表时创建此列的DDL
*/
String columnDefinition() default "";
/**
*/
String table() default "";
/**
*/
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
}
@OneToOne 实体和实体是1对1关联
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface OneToOne {
/**
*/
Class targetEntity() default void.class;
/**
指定关联操作,查询不需要关联操作
*/
CascadeType[] cascade() default {};
/**
关联表是否立即加载,默认立即加载
*/
FetchType fetch() default EAGER;
/**
*/
boolean optional() default true;
/**
*/
String mappedBy() default "";
/**
*/
boolean orphanRemoval() default false;
}
@Entity
@Data
public class CargoOrder {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
//设置关联是懒加载的方式
@OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
//设置外键去关联 customer 表,不是用此注解会生成默认的外键
@JoinColumn(name = "customer_id")
private Customer customer;
}
@Data
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 6286816358723854706L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
//一般我们的关联关系都是单个的
//@OneToOne(cascade = CascadeType.ALL)
//public CargoOrder cargoOrder;
}
var myRepository = context.getBean(MyRepository.class);
//执行下面语句后,session 已经结束
var result= myRepository.findById(1L);
result.ifPresent(p->{
System.out.println(p.getName());
//查询关联实体会失败, no session 错误
// hibernate 给的解决方法是 JpaProperties hibernate.enable_lazy_load_no_trans 为 true
// 这种方式在关联对象是 @OneToOne 的时候是可以解决问题的 但是在 @OneToMany 的时候会有n +1 的问题
// 开启 JpaProperties hibernate.enable_lazy_load_no_trans,会单独开启会话去查询关联表
System.out.println(p.getCustomer().getName());
});
@OneToMany 实体和实体是1对多关联
@OneToMany 1的一方没有办法维护多的一方的所有外键,但是在多的一方,可以维护1的一方的外键,如果在多的一方维护外键关系用 @ManyToOne 维护,此种情况 hibernate 会生成一张中间表来维护外键之间的关系 数据库中间表的表名是 CargoOrder_Customer 字段是: CargoOrder_Customer,customers_id
@Entity
@Data
public class CargoOrder {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
private Set<Customer> customers=new HashSet<Customer>();
}
@Setter
@Getter
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 6286816358723854706L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
@ManyToOne
private CargoOrder cargoOrder;
}
解决方法1: 1端维护外键,但是外键字段定义在多方表里,* 在多方表插入数据后1端会更新多方表的外键字段*
//1的一方
@Entity
@Setter
@Getter
public class CargoOrder {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
//OneToMany 默认是懒加载
@OneToMany(cascade = CascadeType.ALL)
//给 Customer 设置外键指向 CargoOrder
//hibernate 知道这里设置外键肯定不是给 CargoOrder 设置外键 指向 Customer
@JoinColumn(name = "cargoOrderID")
private Set<Customer> customers=new HashSet<Customer>();
}
//多的一方,不需要做其他配置
@Data
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 6286816358723854706L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
}
解决方法2: 1端放弃维护外键,有多方自己维护外键,此外键字段在多方表里, 外键的插入是在多方新增数据的时候,同时插入的
//1端
@Entity
@Setter
@Getter
public class CargoOrder implements Serializable {
private static final long serialVersionUID = 458774547746966971L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
/*
mappedBy 告知 jpa 外键关系由 Customer 表的 cargoOrder 属性来维护
Customer_.CARGO_ORDER 这个是 jpa metamodel 插件提供的功能
*/
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,mappedBy = Customer_.CARGO_ORDER)
//这里不能在定义外键列了
private Set<Customer> customers=new HashSet<Customer>(0);
}
//多端
@Entity
@Setter
@Getter
public class Customer implements Serializable {
private static final long serialVersionUID = 5227592687670264157L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
//此处的外键列可以不定义,用默认值
@JoinColumn(name = "cargoOrderID")
private CargoOrder cargoOrder;
}
测试
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
MyRepository myRepository = context.getBean(MyRepository.class);
val cargoOrder = new CargoOrder();
cargoOrder.setName("iphone");
var cus=new HashSet<Customer>();
cus.add(new Customer(){{
setName(LocalDateTime.now().toString());
//有多端来维护外键,如果不这样写多端表里的外键将为空,因为 mappedBy 已经告知 jpa ,外键有多端维护
setCargoOrder(cargoOrder);
}});
cargoOrder.setCustomers(cus);
myRepository.save(cargoOrder);
{
var result= myRepository.<CargoOrder>findById(1L);
result.ifPresent(p->{
val customer = new Customer();
customer.setName(LocalDateTime.now().toString());
/*
如下在多端维护外键的时候,会报在临时对象的属性里赋值持久性或分离对象不被允许
customer 是灵思对象,p 是分离对象,因为此时事务已经结束
解决方法:JpaProperties 属性增加 put("hibernate.event.merge.entity_copy_observer","allow");
*/
customer.setCargoOrder(p);
p.getCustomers().add(customer);
myRepository.save(p);
System.out.println(p);
});
}
}
}
如上俩种方式是互斥的不能混着写,还有就是 lombok 的 @ToString @EqualsAndHashCode 和 hibernate 兼容有问题
@OrderBy 对多端的返回进行排序
一般和@OneToMany一起使用,对多端返回的数据排序
@Entity
@Setter
@Getter
public class CargoOrder implements Serializable {
private static final long serialVersionUID = 458774547746966971L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,mappedBy = Customer_.CARGO_ORDER)
//指定返回的多端的排序方式
@OrderBy(Customer_.NAME+" asc,"+Customer_.ID+" asc")
private Set<Customer> customers=new HashSet<Customer>(0);
}
@JoinTable 自定义中间表,基本不用
@JoinTable 注解用于关联映射,它是在关联的拥有方进行配置。使用 @JoinTable 注解将创建一个连接表,也称为“中间表”。
@Entity
@Setter
@Getter
public class CargoOrder implements Serializable {
private static final long serialVersionUID = 458774547746966971L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
//OneToMany 默认会生成中间表,@JoinTable 注解可以自定义中间表的表名,字段等
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
//自定义中间表
@JoinTable(name = "a_b")
private Set<Customer> customers=new HashSet<Customer>(0);
}
@ManyToMany 多对多-必须要有中间表,基本不用
ManyToMany总是使用中间关系连接表来存储关系。如果两个实体都定义了ManyToMany的话,因为单向关系,会生成有2个中间表。所以需要用 mappedBy 改造,使其只存在一个中间表
jpa-hibernate 缓存
在 JPA2.0 中,缓存分为一级缓存和二级缓存(JPA1.0 只支持一级缓存)
在 JPA 中,持久化上下文(EntityManager)就是 JPA 的一级缓存。在该缓存区中,会将查询到的对象缓存到该区域中。
一级缓存
如果在同一个 EntityManager 中,查询相同 OID 的数据,那么只需要发送一条 sql。在事务提交/关闭 EntityManager 之后,一级缓存会清空。所以在不同的 EntityManager(或者上下文) 中使用不同的一级缓存。JPA 中的一级缓存的缓存能力是非常有限的,因为我们不会经常在一个 EntityManager 中查询相同的数据。
JPA 中的一级缓存也可以使用下面的方法手动清除缓存数据:
- detach:清除一级缓存中指定的对象
- clear:清除一级缓存中的所有的缓存数据
二级缓存
JPA 中的二级缓存通常是用来提高应用程序性能的,它可以避免访问已经从数据库加载的数据,提高访问未被修改数据对象的速度,JPA 二级缓存是跨越持久化上下文的,是真正意义上的全局应用缓存 如果二级缓存激活,JPA 会先从一级缓存中查找实体,未找到再从二级缓存中查找。当二级缓存有效时,就不能依靠事务来保护并发的数据,而是依靠锁策略,如在确认修改后,需要手工处理乐观锁失败等。
JPA 二级缓存通常用来提高性能,同时,使用二级缓存可能会导致提取到“陈旧”数据,也会出现并发写的问题。所以二级缓存最好是用在经常读取的数据,比较少更新数据的情况,而不应该对重要数据使用二级缓存.