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 的实例有了不同的状态,也就是我们所说的实体实例的生命周期
- PersistenceContext 是持久化上下文,是 JPA 协议定义的,而 Hibernate 的实现是通过 Session 创建和销毁的,也就是说一个 Session 有且仅有一个 PersistenceContext;
- PersistenceContext 既然是持久化上下文,里面管理的是 Entity 的状态;
- EntityManager 是通过 PersistenceContext 创建的,用来管理 PersistenceContext 中 Entity 状态的方法,离开 PersistenceContext 持久化上下文,EntityManager 没有意义;
- 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();