spring data jpa
👍 orm 框架
2022/5/26 10:25:37
➡️

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

  1. SessionFactory(会话工厂) 是一个线程安全的,immutable(终态的),它是一个代理,表示应用程序域,SessionFactory 的建立代价很大,所以一个 应用只能有一个 SessionFactory,SessionFactory 维护 ,Hibernate 的所有 Session(会话),二级缓冲,连接池,事务等等。如果应用需要使用Hibernate访问多个数据库,则需要对每一个数据库使用一个SessionFactory。

  2. Session(会话) Session(会话)是一个单线程,短生命周期的对象,是按"Unit of Work(工作单元),Session 维护了一级缓存,在第一次发送SQL语句查询后第二次直接使用缓存中的数据,不会再发送SQL。除非session缓存被清空,平常的 crud 就通过它。session是一个轻量级对象,Session实例并不是线程安全的,因此应该被设计为每次只能在一个线程中使用。通常将每一个Session实例和一个数据库事务绑定,也就是说,每执行一个数据库事务,都应该先创建一个新的Session实例

  3. Transaction(事务) 那么每个Session的操作,是一个独立的事务,是Hibernate的数据库事务接口,它对底层的事务接口做了封装,底层事务接口包括JDBC事务和JTA(Java Transaction API)事务

  4. Query (接口) 是Hibernate的查询接口,用于向数据库查询对象,以及控制执行查询的过程。Query实例包装了一个HQL(Hibernate Query Language)查询语句,HQL查询语句与SQL查询语句有些相似,但HQL查询语句是面向对象的,它引用类名及类的属性名,而不是表名及表的字段名。 Query接口让你方便地对数据库及持久对象进行查询,它可以有两种表达方式:HQL语言或本地数据库的SQL语句。Query经常被用来绑定查询参数、限制查询记录数量,并最终执行查询操作。

  5. Criteria(接口)

  6. 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 的主要工作就是让实体对象在不同的状态之间进行互相切换

  1. 瞬时状态:实体-没有ID,没有与Session关联

  2. 持久化状态:实体-有ID,与Session关联,特点:持久化状态对象的任何改变都会同步到数据库中 在Hibernate中,持久对象是具有数据库标识且在Session中的实例,持久对象有一个主键值设为数据库标识符。持久对象可能来自瞬时对象被save或saveOrUpdate之后形成,也可能是通过get()或load()等方法从数据库中检索出来的对象。

  3. 游离态:实体-有ID,没有与Session关联.游离态实例表示曾经与某个持久化上下文发生过关联,不过那个上下文被关闭了。它拥有持久化标识,并且在数据库中通常还存在一个对应的行,只是它已经不在持久化层的管理之下。

  4. 删除状态

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 区别

注意点

  1. update 更新 dispatch 对象,此对象会变成持久对象 更新 persistent 对象没有意义 更新 transient 对象,如果此对象有id,更新后除了设定更新的字段,其他字段会重置成null,无id 不能执行会报错

  2. 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 里的事务

注意点:

  1. @PersistenceContext 获取 EntityManager 代理,是线程安全的
  2. @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底层做动态代理的时候发现只要是它的子类或者实现类,都代表储存库操作

Repository<T,ID> 类型关系图

注意:

  1. @NoRepositoryBean 在接口或对象上加此注解的 jpa 存储库,不会注入 spring ioc
  2. @Repository 如果在加了 @NoRepositoryBean 接口的实现类注入 spring ioc ,要把 @Repository 加载类或接口上
  3. 为什么存储库的方法只用会有事务,因为 SimpleJpaRepository 实现类上加了注解 @Transactional

Repository 大类:

  1. ReactiveCrudRepository,响应式编程,主要支持当前 NoSQL 方面的操作,因为这方面大部分操作都是分布式的,目前 Reactive 主要有 Cassandra、MongoDB、Redis 的实现。
  2. CoroutineCrudRepository :为了支持 Kotlin 语法而实现的。
  3. CrudRepository :JPA 相关的操作接口,也是我们主要用到的接口

更详细一点,我们需要掌握和使用到的7 大 Repository 接口如下所示:

  1. Repository(org.springframework.data.repository),没有暴露任何方法;
  2. CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
  3. PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
  4. QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
  5. JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
  6. JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;

两大 Repository 实现类:

  1. SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类;

第三方的 QueryDsl 在 JPA 和 spring data 的基础上扩展的第三方查询库:

  1. 接口 QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装
  2. 实现 QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类

spring 扩展的方法

@NamedQuery、@Query和方法定义查询的对比:

  1. Spring JPA里面的优先级,@Query > @NameQuery > 方法定义查询。
  2. 推荐使用的优先级:@Query > 方法定义查询 > @NameQuery。
  3. 相同点是都不支持动态条件查询。

方法的查询策略设置 @EnableJpaRepositories 注解来配置方法的查询策略,详细配置方法如下: @EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)

其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:

  1. Create:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则, 启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有用的。
  2. USE_DECLARED_QUERY:声明方式创建,启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,可以理解为必须配置 @Query 或 @NameQuery
  3. 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 ages)
NotIn findByAgeNotIn(Collection ages)
True findByActiveTrue()
False findByActiveFalse()
IgnoreCase findByFirstnameIgnoreCase

综上,总结 3 点经验:

  1. 方法名的表达式通常是实体属性连接运算符的组合,如 And、or、Between、LessThan、GreaterThan、Like 等属性连接运算表达式,不同的数据库(NoSQL、MySQL)可能产生的效果不一样,如果遇到问题,我们可以打开 SQL 日志观察。
  2. IgnoreCase 可以针对单个属性(如 findByLastnameIgnoreCase(…)),也可以针对查询条件里面所有的实体属性忽略大小写(所有属性必须在 String 情况下,如 findByLastnameAndFirstnameAllIgnoreCase(…))。
  3. 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. ?1 传入的参数是方法的第一个参数 ,序号参数
  2. :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 也有几个限制:

  1. 不支持嵌套或分组的属性约束,例如firstname = ?0 or (firstname = ?1 and lastname = ?2)
  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 findPersonByName(String name, Sort sort); }

@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()));
  }
}

查询返回结果

常见的返回结果:

  1. List
  2. Slice
  3. Page
  4. Stream
  5. CompletableFuture
  6. 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注解将实体类的多个属性分别封装到不同的非实体类中,纯粹的继承,和表没关系,对象之间的字段共享 注意:

  1. 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
  2. 标注为@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 -不用,增加复杂度

  1. @Inheritance (strategy = InheritanceType.SINGLE_TABLE) 将所有父类和子类集合在一张表,只有父类有 @Table ,其他表的字段集合到父类 ,在父类里加字段进行区分子类
  2. @Inheritance (strategy = InheritanceType.TABLE_PER_CLASS) 每个子类会生成一张单独的表,父类可以查询所有子类的表数据
  3. @Inheritance (strategy = InheritanceType.JOINED) 每个类分别生成一张单独的表,但是每张表只有自己的属性,没有父类的属性,通过外键关联的形式使两张表关联起来

jpa-hibernate @Entity Callbacks-的回调方法

JPA 协议里面规定,可以通过一些注解,为其监听回调事件、指定回调方法

注解 备注
@PrePersist 在新增之前的回调
@PostPersist 在新增到数据库之前的回调
@PreRemove 在数据库删除之前的回调
@PostRemove 在数据库删除成功之后的回调
@PreUpdate 在更新到数据库之前回调
@PostUpdate 在数据库更新之后的回调
@PostLoad 在实体加载之后的回调

语法注意事项,关于上表所述的几个方法有一些需要注意的地方,如下:

  1. 回调函数都是和 EntityManager.flush 或 EntityManager.commit 在同一个线程里面执行的,只不过调用方法有先后之分,都是同步调用,所以当任何一个回调方法里面发生异常,都会触发事务进行回滚,而不会触发事务提交。

  2. Callbacks 注解可以放在实体里面,可以放在 super-class 里面,也可以定义在 entity 的 listener 里面,但需要注意的是:放在实体(或者 super-class)里面的方法,签名格式为“void ()”,即没有参数,方法里面操作的是 this 对象自己;放在实体的 EntityListener 里面的方法签名格式为“void (Object)”,也就是方法可以有参数,参数是代表用来接收回调方法的实体。

  3. 使上述注解生效的回调方法可以是 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 注解同样可以做到联合主键的效果,并且更方便

作为符合主键类,要满足以下几点要求。

  1. 必须实现Serializable接口。
  2. 必须有默认的public无参数的构造方法。
  3. 必须覆盖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 注解支持以下数据库类型的字段:

  1. Clob( Character Large Ojects) 类型是长字符串类型,java.sql.Clob、Character[]、 char[] 和 String 将被映射为 Clob 类型。
  2. 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 值对象

注意点:

  1. @Embeddable 定义没有主键的值对象
  2. @Embedded 把值对象嵌入实体类里面
  3. @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 中的一级缓存也可以使用下面的方法手动清除缓存数据:

  1. detach:清除一级缓存中指定的对象
  2. clear:清除一级缓存中的所有的缓存数据

二级缓存

JPA 中的二级缓存通常是用来提高应用程序性能的,它可以避免访问已经从数据库加载的数据,提高访问未被修改数据对象的速度,JPA 二级缓存是跨越持久化上下文的,是真正意义上的全局应用缓存 如果二级缓存激活,JPA 会先从一级缓存中查找实体,未找到再从二级缓存中查找。当二级缓存有效时,就不能依靠事务来保护并发的数据,而是依靠锁策略,如在确认修改后,需要手工处理乐观锁失败等。

JPA 二级缓存通常用来提高性能,同时,使用二级缓存可能会导致提取到“陈旧”数据,也会出现并发写的问题。所以二级缓存最好是用在经常读取的数据,比较少更新数据的情况,而不应该对重要数据使用二级缓存.

👍🎉🎊