spring data jpa 学习(二)

Auditing

Auditing 是帮我们做审计用的,主要就是创建人,修改人,创建时间,修改时间做自动的填充

@CreatedBy 是哪个用户创建的。

@CreatedDate 创建的时间。

@LastModifiedBy 最后修改实体的用户。

@LastModifiedDate 最后一次修改的时间。

首先在users表里面添加这四个字段,然后修改实体

@Entity
@Data
@EntityListeners(AuditingEntityListener.class)//不能少
public class Users {
   @Id
   @GeneratedValue(strategy= GenerationType.IDENTITY)
   private Long userid;
   private String username;
   private Integer age;
   private String address;

    //可以将公共的字段写到基类里面
   @CreatedBy
   private Integer createid;
   @LastModifiedBy
   private Integer updateid;
   @CreatedDate
   private Date createtime;
   @LastModifiedDate
   private Date updatetime;
}

实现AuditorAware接口,以及getCurrentAuditor方法

public class MyAuditorAware implements AuditorAware<Integer> {
   @Override
   public Optional<Integer> getCurrentAuditor() {
      Optional<Integer> integer = Optional.of(2);
      return integer;
   }
}

开启审计配置

@Configuration
@EnableJpaAuditing
public class JpaConfiguration {
   @Bean
   @ConditionalOnMissingBean(name = "myAuditorAware")
   MyAuditorAware myAuditorAware() {
      return new MyAuditorAware();
   }
}

回调函数

@PrePersist //新增之前执行
@PreRemove //删除之前执行
@PreUpdate //修改之前执行
@PostLoad //数据加载之后执行
@PostPersist //新增之后执行
@PostRemove //删除之后执行
@PostUpdate //修改之后执行

回调函数都是和 EntityManager.flush 或 EntityManager.commit 在同一个线程里面执行的,只不过调用方法有先后之分,都是同步调用,所以当任何一个回调方法里面发生异常,都会触发事务进行回滚,而不会触发事务提交,也就是说如果回调函数发生异常的话会导致当前的事务操作会回滚,一定要做异常处理。

使上述注解生效的回调方法可以是 public、private、protected、friendly 类型的,但是不能是 static 和 finnal 类型的方法。

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

//这是放在实体里面的回调函数
@PreUpdate
public void preUpdate() {
    System.out.println("preUpdate::"+this.toString());
    this.setCreateUserId(200);//this代表实体本身
}
//如果自定义的EntityListener里面的话,回调函数时我们可以加上参数,这个参数可以是父类 Object,可以是基类(BaseEntity),也可以是具体的某一个实体;我推荐用 BaseEntity,因为这样的方法是类型安全的,它可以约定一些框架逻辑,比如 getCreateUserId、getLastModifiedUserId 等。

乐观锁

使用@version注解就可以实现乐观锁,就是在做数据库更新时,sql后面会自动带一个version字段的判断,并且会更新version字段

select userid,name,version from user where id=1;
update user set name='jack', version=version+1 where userid=1 and version=1

加上该注解后,如果更新不成功就会抛出异常OptimisticLockException

注意:Spring Data JPA 里面有两个 @Version 注解,请使用 @javax.persistence.Version,而不是 @org.springframework.data.annotation.Version。

在save方法里面判断时新增还是修改时,是先判断是否有 @Version 注解的字段,如果有 @Version 注解的字段,就是用该字段做新增修改的判断,如果没有就是用@id注解的字段来判断是新增还是修改

如果发生了异常我们可以使用重试机制,引入spring-retry依赖

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.5.RELEASE</version><!--版本自己选择-->
</dependency>

然后在service层的方法上面加上@Retryable注解即可

新增一个RetryConfiguration并添加@EnableRetry 注解,开启重试机制。

@EnableRetry
@Configuration
public class RetryConfiguration {
}
//标注了发生异常的类型为ObjectOptimisticLockingFailureException才开启重试机制,backoff 采用随机 +1.5 倍的系数,这样基本很少会出现连续 3 次乐观锁异常的情况,并且也很难发生重试风暴而引起系统重试崩溃的问题
//推荐使用
@Retryable(value = ObjectOptimisticLockingFailureException.class,backoff = @Backoff(multiplier = 1.5,random = true))

对spring mvc的支持

1、支持在 Controller 层直接返回实体,而不使用其显式的调用方法;

2、对 MVC 层支持标准的分页和排序功能;

3、扩展的插件支持 Querydsl,可以实现一些通用的查询逻辑。

@EnableSpringDataWebSupport是开启该配置的注解,不过springboot的自动加载机制会默认加载改功能,也就是说如果是springboot+MVC+JPA的话,是不需要我们做什么的

DomainClassConverter组件

DomainClassConverter就是把我们路径中的参数自动的拿去查询(根据id)并返回实体

@RestController
public class UserController {
   @Autowired
   private UserService userService;

   @RequestMapping("/finduser/{id}")
   public Users finduserById(@PathVariable("id") Users users) {
      return users;
   }

   @RequestMapping("/finduser")
   public Users finduser(@RequestParam("id") Users users) {
      return users;
   }
}

在DomainClassConverter类里面的ToEntityConverter类里面有一个matches方法,这个方法就是先判断参数类型是不是实体,并且有没有对应的实体 Repositorie 存在,如果不存在,就会直接报错说找不到合适的参数转化器。如果匹配到了就会调用convert方法,然后在调用FindById(id)查询实体

public class DomainClassConverter<T extends ConversionService & ConverterRegistry> implements ConditionalGenericConverter, ApplicationContextAware {
    private static class ToEntityConverter implements ConditionalGenericConverter {
        
        public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            if (sourceType.isAssignableTo(targetType)) {
                return false;
            } else {
                Class<?> domainType = targetType.getType();
                if (!this.repositories.hasRepositoryFor(domainType)) {
                    return false;
                } else {
                    Optional<RepositoryInformation> repositoryInformation = this.repositories.getRepositoryInformationFor(domainType);
                    return (Boolean)repositoryInformation.map((it) -> {
                        Class<?> rawIdType = it.getIdType();
                        return sourceType.equals(TypeDescriptor.valueOf(rawIdType)) || this.conversionService.canConvert(sourceType.getType(), rawIdType);
                    }).orElseThrow(() -> {
                        return new IllegalStateException(String.format("Couldn't find RepositoryInformation for %s!", domainType));
                    });
                }
            }
        }
        @Nullable
        public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            if (source != null && StringUtils.hasText(source.toString())) {
                if (sourceType.equals(targetType)) {
                    return source;
                } else {
                    Class<?> domainType = targetType.getType();
                    RepositoryInvoker invoker = this.repositoryInvokerFactory.getInvokerFor(domainType);
                    RepositoryInformation information = this.repositories.getRequiredRepositoryInformation(domainType);
                    Object id = this.conversionService.convert(source, information.getIdType());
                    return id == null ? null : invoker.invokeFindById(id).orElse((Object)null);
                }
            } else {
                return null;
            }
        }
    }
}

还有分页和排序的支持

@RequestMapping("/users")
public Page<Users> queryByPage(Pageable pageable, Users users) {
   return userInfoRepository.findAll(Example.of(users),pageable);
}

@RequestMapping("/users/sort")
public HttpEntity<List<Users>> queryBySort(Sort sort) {
   return new HttpEntity<>(userInfoRepository.findAll(sort));
}

@DynamicInsert 和 @DynamicUpdate

根据实体字段是否为空动态生成sql,也就是说如果实体某一字段为空的话,会不更新这个字段,只更新有值的字段

如果不加这个注解的话,字段为空就会在数据库更新为空

日志

### 日志级别的灵活运用
## hibernate相关
# 显示sql的执行日志,如果开了这个,show_sql就可以不用了
logging.level.org.hibernate.SQL=debug
# hibernate id的生成日志
logging.level.org.hibernate.id=debug
# hibernate所有的操作都是PreparedStatement,把sql的执行参数显示出来
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# sql执行完提取的返回值
logging.level.org.hibernate.type.descriptor.sql=trace
# 请求参数
logging.level.org.hibernate.type=debug
# 缓存相关
logging.level.org.hibernate.cache=debug
# 统计hibernate的执行状态
logging.level.org.hibernate.stat=debug
# 查看所有的缓存操作
logging.level.org.hibernate.event.internal=trace
logging.level.org.springframework.cache=trace
# hibernate 的监控指标日志
logging.level.org.hibernate.engine.internal.StatisticalLoggingSessionEventListener=DEBUG
### 连接池的相关日志
## hikari连接池的状态日志,以及连接池是否完好 #连接池的日志效果:HikariCPPool - Pool stats (total=20, active=0, idle=20, waiting=0)
logging.level.com.zaxxer.hikari=TRACE
#开启 debug可以看到 AvailableSettings里面的默认配置的值都有哪些,会输出类似下面的日志格式
# org.hibernate.cfg.Settings               : Statistics: enabled
# org.hibernate.cfg.Settings               : Default batch fetch size: -1
logging.level.org.hibernate.cfg=debug
#hikari数据的配置项日志
logging.level.com.zaxxer.hikari.HikariConfig=TRACE
### 查看事务相关的日志,事务获取,释放日志
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=TRACE
logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG
### 分析connect 以及 orm和 data的处理过程更全的日志
logging.level.org.springframework.data=trace
logging.level.org.springframework.orm=trace

Persistence Context

Persistence Context是用来管理会话里面的 Entity 状态的一个上下文环境,使 Entity 的实例有了不同的状态,也就是我们所说的实体实例的生命周期

  1. PersistenceContext 是持久化上下文,是 JPA 协议定义的,而 Hibernate 的实现是通过 Session 创建和销毁的,也就是说一个 Session 有且仅有一个 PersistenceContext;
  2. PersistenceContext 既然是持久化上下文,里面管理的是 Entity 的状态;
  3. EntityManager 是通过 PersistenceContext 创建的,用来管理 PersistenceContext 中 Entity 状态的方法,离开 PersistenceContext 持久化上下文,EntityManager 没有意义;
  4. EntityManger 是操作对象的唯一入口,一个请求里面可能会有多个 EntityManger 对象。

Entity 在 PersistenceContext 里面有不同的状态。对此,JPA 协议定义了四种状态:new、manager、detached、removed。

第一种:New 状态的对象

当我们使用关键字 new 的时候创建的实体对象,称为 new 状态的 Entity 对象。它需要同时满足两个条件:new 状态的实体 Id 和 Version 字段都是 null;new 状态的实体没有在 PersistenceContext 中出现过。

那么如果我们要把 new 状态的 Entity 放到 PersistenceContext 里面,有两种方法:执行 entityManager.persist(entity) 方法;通过关联关系的实体关系配置 cascade=PERSIST or cascade=ALL 这种类型,并且关联关系的一方,也执行了 entityManager.persist(entity) 方法。

第二种:Detached(游离)的实体对象

Detached 状态的对象表示和 PersistenceContext 脱离关系的 Entity 对象。它和 new 状态的对象的不同点在于:

  • Detached 是 new 状态的实体对象,有持久化 ID(即有 ID );
  • 变成持久化对象需要进行 merger 操作,merger 操作会 copy 一个新的实体对象,然后把新的实体对象变成 Manager 状态。

而 Detached 和 new 状态的对象相同点也有两个方面:

  • 都和 PersistenceContext 脱离了关系;
  • 当执行 flush 操作或者 commit 操作的时候,不会进行数据库同步。

如果想让 Manager(persist) 状态的对象从 PersistenceContext 里面游离出来变成 Detached 的状态,可以通过 EntityManager 的 Detach 方法实现,如下面这行代码。

entityManager.detach(entity);

当执行完 entityManager.clear()、entityManager.close(),或者事务 commit()、事务 rollback() 之后,所有曾经在 PersistenceContext 里面的实体都会变成 Detached 状态。

而游离状态的对象想回到 PersistenceContext 里面变成 manager 状态的话,只能执行 entityManager 的 merge 方法,也就是下面这行代码。

entityManager.merge(entity);

游离状态的实体执行 EntityManager 中 persist 方法的时候就会报异常

第三种:Manager(persist) 状态的实体

Manager 状态的实体,顾名思义,是指在 PersistenceContext 里面管理的实体,而此种状态的实体当我们执行事务的 commit(),或者 entityManager 的 flush 方法的时候,就会进行数据库的同步操作。可以说是和数据库的数据有映射关系。

New 状态如果要变成 Manager 的状态,需要执行 persist 方法;而 Detached 状态的实体如果想变成 Manager 的状态,则需要执行 merge 方法。在 session 的生命周期中,任何从数据库里面查询到的 Entity 都会自动成为 Manager 的状态,如 entityManager.findById(id)、entityManager.getReference 等方法。

而 Manager 状态的 Entity 要同步到数据库里面,必须执行 EntityManager 里面的 flush 方法。也就是说我们对 Entity 对象做的任何增删改查,必须通过 entityManager.flush() 执行之后才会变成 SQL 同步到 DB 里面

第四种:Removed 的实体状态

Removed 的状态,顾名思义就是指删除了的实体,但是此实体还在 PersistenceContext 里面,只是在其中表示为 Removed 的状态,它和 Detached 状态的实体最主要的区别就是不在 PersistenceContext 里面,但都有 ID 属性。

而 Removed 状态的实体,当我们执行 entityManager.flush() 方法的时候,就会生成一条 delete 语句到数据库里面。Removed 状态的实体,在执行 flush() 方法之前,执行 entityManger.persist(removedEntity) 方法时候,就会去掉删除的表示,变成 Managed 的状态实例

Flush 的作用

flush 重要的、唯一的作用,就是将 Persistence Context 中变化的实体转化成 sql 语句,同步执行到数据库里面。换句话来说,如果我们不执行 flush() 方法的话,通过 EntityManager 操作的任何 Entity 过程都不会同步到数据库里面。

而 flush() 方法很多时候不需要我们手动操作,这里我直接通过 entityManager 操作 flush() 方法,仅仅是为了向你演示执行过程。实际工作中很少会这样操作,而是会直接利用 JPA 和 Hibernate 底层框架帮我们实现的自动 flush 的机制。

Flush 的时候会改变 SQL 的执行顺序:insert 的先执行、delete 的第二个执行、update 的第三个执行。

DataSource

Spring Boot默认的数据源使用的是:Hikari

HikariDataSource

public class HikariDataSource extends HikariConfig implements DataSource, Closeable {
    private static final Logger LOGGER = LoggerFactory.getLogger(HikariDataSource.class);
    private final AtomicBoolean isShutdown = new AtomicBoolean();
    private final HikariPool fastPathPool;
    private volatile HikariPool pool;

    public HikariDataSource() {
        this.fastPathPool = null;
    }

    public HikariDataSource(HikariConfig configuration) {
        configuration.validate();
        configuration.copyStateTo(this);
        LOGGER.info("{} - Starting...", configuration.getPoolName());
        this.pool = this.fastPathPool = new HikariPool(this);
        LOGGER.info("{} - Start completed.", configuration.getPoolName());
        this.seal();
    }

    public Connection getConnection() throws SQLException {
        if (this.isClosed()) {
            throw new SQLException("HikariDataSource " + this + " has been closed.");
        } else if (this.fastPathPool != null) {
            return this.fastPathPool.getConnection();
        } else {
            HikariPool result = this.pool;
            if (result == null) {
                synchronized(this) {
                    result = this.pool;
                    if (result == null) {
                        this.validate();
                        LOGGER.info("{} - Starting...", this.getPoolName());
                        try {
                            this.pool = result = new HikariPool(this);
                            this.seal();
                        } catch (PoolInitializationException var5) {
                            if (var5.getCause() instanceof SQLException) {
                                throw (SQLException)var5.getCause();
                            }
                            throw var5;
                        }
                        LOGGER.info("{} - Start completed.", this.getPoolName());
                    }
                }
            }
            return result.getConnection();
        }
    }

    public Connection getConnection(String username, String password) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }
}

HikariConfig是给Hikari做配置的,用户名、密码、连接池的配置、jdbcUrl、驱动的名字这些都是在这里面配置的

连接池通过数据源的配置创建连接,数据源通过连接池获取连接,程序通过数据源获取连接,通过连接和驱动操作数据库

DataSourceAutoConfiguration

@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
    public DataSourceAutoConfiguration() {
    }

    @Configuration(
        proxyBeanMethods = false
    )
    @Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, Generic.class, DataSourceJmxConfiguration.class})
    protected static class PooledDataSourceConfiguration {
        protected PooledDataSourceConfiguration() {
        }
    }

    @Configuration(
        proxyBeanMethods = false
    )
    @Conditional({DataSourceAutoConfiguration.EmbeddedDatabaseCondition.class})
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import({EmbeddedDataSourceConfiguration.class})
    protected static class EmbeddedDatabaseConfiguration {
        protected EmbeddedDatabaseConfiguration() {
        }
    }
}

DataSourceProperties可以找到spring.datasource 的配置项有哪些

N+1 SQL

如果遇到有关联关系的,如果一对多,多对一,加入A表一对多B表,也就是说一条A表信息对应到B表可能有多条,这种在执行查询A表有N条的时候,A表会查询一次,B表会查询N次,也就是N+1 条SQL,这样查询效率肯定不好

# 更改批量取数据的大小为20
spring.jpa.properties.hibernate.default_batch_fetch_size= 20

可以通过上诉的配置解决这种问题

这个配置会把A表查询出来的信息在查询B表时以 In (?,?,?) 的形式查询

也可以使用@BatchSize 注解,该注解可以加上实体类上和关联关系属性上面

实体类上加@BatchSize注解,用来设置当被关联关系的时候一次查询的大小;属性上加@BatchSize注解,用来设置当通过当前实体加载关联实体的时候一次取数据的大小

在配置文件的是全局配置,使用注解式局部配置(注解对于@ManyToOne 和 @OneToOne是不起作用的)

还可以使用@Fetch 注解(hibernate提供的),@Fetch(value = FetchMode.SELECT)

FetchMode.SELECT(默认),代表获取关系的时候新开一个 SQL 进行查询。就是N+1 SQL

FetchMode.JOIN,代表主表信息和关联关系通过一个 SQL JOIN 的方式查出来,1条SQL(FetchMode.JOIN 只支持通过 ID 或者联合唯一键获取数据才有效)

FetchMode.SUBSELECT,代表将关联关系通过子查询的形式查询出来,和上面@BatchSize有点类似

使用@EntityGraph

@NamedEntityGraphs(value = {@NamedEntityGraph(name = "addressGraph",attributeNodes = @NamedAttributeNode(value = "addressList"))})//放在实体上

//在*Repository 的方法上面直接使用 @EntityGraph
@Override
//我们指定EntityGraph引用的是,在UserInfo实例里面配置的name=addressGraph的NamedEntityGraph;
// 这里采用的是LOAD的类型,也就是说被addressGraph配置的实体图属性address采用的fetch会变成 FetchType.EAGER模式,而没有被addressGraph实体图配置关联关系属性room还是采用默认的EAGER模式
@EntityGraph(value = "addressGraph",type = EntityGraph.EntityGraphType.LOAD)
List<UserInfo> findAll();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值