Spring Boot 集成JPA一篇就够

    科技2022-08-15  101

    目前市场上Java语言ORM框架有Mybatis、Hibernate、Spring Data JPA,其中JPA底层还是使用Hibernate实现,引用JPQL查询语言,属于Spring整个生态体系的一部分,使用起来比较方便,加快了研发效率。

    Part-1: Jpa基础知识

    本章节将通过一些例子让大家对Jpa的日常使用有一个了解。

    数据库准备 CREATE TABLE `user_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `first_name` varchar(200) DEFAULT NULL, `last_name` varchar(200) DEFAULT NULL, `create_time` datetime NOT NULL, `create_user_id` bigint NOT NULL, `last_upgrade_time` datetime DEFAULT NULL, `last_upgrade_user_id` bigint DEFAULT NULL, `delete_time` datetime DEFAULT NULL, `delete_user_id` bigint DEFAULT NULL, `is_deleted` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=115 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

    配置JPA

    pom文件引用 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> 数据库连接 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://host:port/DBSchema spring.datasource.username=username spring.datasource.password=password spring.jpa.show-sql=true #是否显示数据库语句

    常用Repository类结构

    主要实现类SimpleJpaRepository和QuerydslJpaRepository,其中QuerydslJpaRepository为框架不赞成使用类。

    注解型查询方法

    分页查询 //声明方法 public interface UserInfoRepository extends JpaRepository<UserInfo, Long> { @Query("select u from UserInfo u where u.firstName like concat('%',:firstname,'%') ") List<UserInfo> findByFirstName(@Param("firstname") String firstName,Pageable pageable); } //调用方 List<UserInfo> userInfos = userInfoRepository.findByFirstName("ray",PageRequest.of(0,10)); 排序查询 //声明方法 public interface UserInfoRepository extends JpaRepository<UserInfo, Long> { @Query("select u from UserInfo u where u.firstName like concat('%',:firstname,'%') ") List<UserInfo> findByFirstName(@Param("firstname") String firstName, Sort sort); } //调用方 Sort sort=Sort.by("lastName").descending() List<UserInfo> softUserInfos=userInfoRepository.findByFirstName("ray",sort);

    实体声明和通用注解

    基础注解 @Entity:代表对象将被JPA管理的实体@Table:指定数据库表@Id定义为数据库的主键,一个实体必须有一个@GeneratedValue为主键生成策略: TABLE:通过表产生主键,框架有表模拟序列产生主键,使用该策略可以使应用更易于数据库移植SEQUENCE:通过序列产生主键,通过@SequenceGenerator指定序列名,MySql不支持这个方法IDENTITY:采用数据库ID增长,一般用于MySql数据库AUTO:JPA自动选择合适的策略,默认选项 @Basic:表示该属性是数据库表的字段映射@Transient:表示属性并非映射到数据库@Column:定义属性映射到数据库列名@Temporal:用来设置Date类型映射到对应精度的字段 TemporalType.DATE:只有日期TemporalType.TIME:只有时间TemporalType.TIMESTAMP:日期+时间 @Enumerated:枚举字段 EnumType.ORDINAL:数组下标,一般枚举后期都会扩展,可能会引起数组下标变化故不推荐使用EnumType.STRING:枚举Name 关联查询 @JoinColumn:定义多个字段关联关系@OneToOne:可以双向关联,也可以仅配置一方@OneToMany与@ManyToOne@OrderBy:关联查询时排序@JoinTable:关联关系表@ManyToMany:代表多对多@EntityGraph:提升关联查询效率,上边的如果有多条数据会出现多条语句查询,可以通过join查询方式提升效率 实体继承

    Part-2:逻辑删除实现方法

    单实体全局处理 @Entity @Table(name = "user_info") @Where(clause = "is_deleted=0") @SQLDelete(sql = "update user_info set is_deleted=1 where id=?") public class UserInfo extends BaseEntity { @Column(name = "first_name", nullable = true) private String firstName; @Column(name = "last_name", nullable = true) private String lastName; ....省略属性Get/Set.... }

    该方法通过@Where注解将所有查询实体都会加上is_deleted=0的查询条件,但是真删除功能会被覆盖

    继承PagingAndSortingRepository实现相关查询接口,增加假删除接口 @NoRepositoryBean public interface MyBaseRepository<T extends BaseEntity, ID extends Serializable> extends PagingAndSortingRepository<T, ID> { @Override @Transactional(readOnly = true) @Query("select e from #{#entityName} e where e.deleted=false") List<T> findAll(); @Override @Transactional(readOnly = false) @Query("select e from #{#entityName} e where e.id in ?1 and e.deleted=false") List<T> findAllById(Iterable<ID> iterable); @Override @Transactional(readOnly = true) @Query("select e from #{#entityName} e where e.deleted=false and e.id=?1") Optional<T> findById(ID id); @Override @Transactional(readOnly = true) default boolean existsById(ID id) { return findById(id) != null; } @Override @Transactional(readOnly = true) @Query("select count(e) from #{#entityName} e where e.deleted=false ") long count(); @Modifying @Query("update #{#entityName} e set e.deleted=true") void logicDeleteAll(); @Transactional @Modifying @Query("update #{#entityName} e set e.deleted=true where e.id=?1") void logicDelete(ID id); @Modifying @Transactional default void logicDelete(T entity) { logicDelete((ID) entity.getId()); } @Transactional default void logicDelete(Iterable<? extends T> entities) { entities.forEach(entity -> { logicDelete((ID) entity.getId()); }); } }

    该方法需要自己在自定义查询方法的时候手动加上is_deleted条件进行过滤

    Part-3:多数据源配置

    配置文件 # 数据库1 spring.datasource.db1.driver-class-name=com.mysql.jdbc.Driver spring.datasource.db1.url=jdbc:mysql://localhost:3306/db1 spring.datasource.db1.username=root spring.datasource.db1.password=12345678 # 数据库2 spring.datasource.db2.driver-class-name=com.mysql.jdbc.Driver spring.datasource.db2.url=jdbc:mysql://localhost:3306/db2 spring.datasource.db2.username=root spring.datasource.db2.password=12345678 配置声明 @Configuration public class PropertieConfig { @Autowired private JpaProperties jpaProperties; @Autowired private HibernateProperties hibernateProperties; @Bean(name = "vendorProperties") public Map<String, Object> getVendorProperties() { return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings()); } } 数据源1配置 @Configuration public class DB1DataSourceConfig { @Bean(name = "db1DataSource") @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource db1DataSource() { return new DruidDataSource(); } } @Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef="db1EntityManagerFactory", transactionManagerRef="db1TransactionManager", basePackages= { "lsf.db1.repository" })//设置dao(repo)所在位置 public class DB1EntryManagerConfig { @Autowired @Qualifier("db1DataSource") private DataSource db1DataSource; @Autowired @Qualifier("vendorProperties") private Map<String, Object> vendorProperties; @Bean(name = "db1EntityManagerFactory") @Primary public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder) { return builder .dataSource(db1DataSource) .properties(vendorProperties) .packages("lsf.db1.entity") //设置实体类所在位置 .persistenceUnit("primaryPersistenceUnit") .build(); } @Bean(name = "db1EntityManager") public EntityManager entityManager(EntityManagerFactoryBuilder builder) { return entityManagerFactoryPrimary(builder).getObject().createEntityManager(); } @Bean(name = "db1TransactionManager") public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) { return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject()); } } 数据源2配置 @Configuration public class DB2DataSourceConfig { @Bean(name = "db2DataSource") @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return new DruidDataSource(); } } @Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef="db2EntityManagerFactory", transactionManagerRef="db2TransactionManager", basePackages= { "lsf.db2.repository" })//设置dao(repo)所在位置 public class DB2EntryManagerConfig { @Autowired @Qualifier("db2DataSource") private DataSource db2DataSource; @Autowired @Qualifier("vendorProperties") private Map<String, Object> vendorProperties; @Bean(name = "db2EntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder) { return builder .dataSource(db2DataSource) .properties(vendorProperties) .packages("lsf.db2.entity") //设置实体类所在位置 .persistenceUnit("primaryPersistenceUnit") .build(); } @Bean(name = "db2EntityManager") public EntityManager entityManager(EntityManagerFactoryBuilder builder) { return entityManagerFactoryPrimary(builder).getObject().createEntityManager(); } @Bean(name = "db2TransactionManager") public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) { return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject()); } }

    Part-4:Druid集成

    Druid引入 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.24</version> </dependency> 配置信息 #拦截器为可视化界面提供数据 spring.datasource.filesystem.filters=stat #初始化连接池个数 spring.datasource.filesystem.initialSize=5 #最小连接池个数 spring.datasource.filesystem.minIdle=5 #最大连接池个数 spring.datasource.filesystem.maxActive=20 # 配置获取连接等待超时的时间,单位毫秒,缺省启用公平锁,并发效率会有所下降 spring.datasource.filesystem.maxWait=60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 spring.datasource.filesystem.timeBetweenEvictionRunsMillis=60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 spring.datasource.filesystem.minEvictableIdleTimeMillis=300000 #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 spring.datasource.filesystem.poolPreparedStatements=false #要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 spring.datasource.filesystem.maxOpenPreparedStatements= -1 #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 spring.datasource.filesystem.testOnBorrow=true #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 spring.datasource.filesystem.testOnReturn=false #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 spring.datasource.filesystem.testWhileIdle=false #物理连接初始化的时候执行的sql spring.datasource.filesystem.connectionInitSqls= #根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接 spring.datasource.filesystem.exceptionSorter #用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 spring.datasource.filesystem.validationQuery 声明监控拦截器 @WebFilter(filterName = "druidWebStatFilter", urlPatterns = "/*", initParams = { @WebInitParam(name = "exclusions", value = "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"), // 忽略资源 }) public class DruidStatFilter extends WebStatFilter { } druid监控视图配置 @WebServlet(urlPatterns = "/druid/*", initParams={ // @WebInitParam(name="allow",value="192.168.6.195"), // IP白名单 (没有配置或者为空,则允许所有访问) // @WebInitParam(name="deny",value="192.168.6.73"), // IP黑名单 (存在共同时,deny优先于allow) @WebInitParam(name="loginUsername",value="admin"), // 用户名 @WebInitParam(name="loginPassword",value="admin"), // 密码 @WebInitParam(name="resetEnable",value="true") // 禁用HTML页面上的“Reset All”功能 }) public class DruidStatViewServlet extends StatViewServlet { private static final long serialVersionUID = 7359758657306626394L; } 启动类添加组件扫描 @SpringBootApplication @ServletComponentScan public class StudyDruidApplication { public static void main(String[] args) { SpringApplication.run(StudyDruidApplication.class, args); } }

    Part-5:事件使用

    添加实体类注解 @Entity @Table(name = "user_info") @EntityListeners({ActionsLogsAuditListener.class}) public class UserInfo extends BaseEntity 声明事件 public class ActionsLogsAuditListener { private static final Logger logger = LoggerFactory.getLogger(ActionsLogsAuditListener.class); @PrePersist private void prePersist(Object entity) { this.notice(entity, OperateType.create,OperateStatus.before); } @PostPersist private void postPersist(Object entity) { notice(entity, OperateType.create,OperateStatus.after); } @PreRemove private void preRemove(Object entity) { notice(entity, OperateType.remove,OperateStatus.before); } @PostRemove private void postRemove(Object entity) { notice(entity, OperateType.remove,OperateStatus.after); } @PreUpdate private void preUpdate(Object entity) { notice(entity, OperateType.update,OperateStatus.before); } @PostUpdate private void postUpdate(Object entity) { notice(entity, OperateType.update,OperateStatus.after); } @PostLoad private void postLoad(Object entity) { this.notice(entity, OperateType.load,OperateStatus.after); } private void notice(Object entity, OperateType operateType, OperateStatus operateStatus) { if (operateStatus == OperateStatus.after) { logger.info("{}执行了{}操作", entity, operateType.getDescription()); } else if (operateStatus == OperateStatus.before) { logger.info("{}准备执行{}操作", entity, operateType); } } enum OperateStatus { before("操作前"), after("操作后"); private final String description; OperateStatus(String description) { this.description = description; } public String getDescription() { return description; } } enum OperateType { create("创建"), remove("删除"), update("修改"), load("查询"); private final String description; OperateType(String description) { this.description = description; } public String getDescription() { return description; } } }

    Part-6:事务Transactional

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) public boolean saveUserInfo() throws Exception 事务传播等级 REQUIRED:当前存在事务则加入,没有事务则创建SUPPORTS:存在事务则加入,没有事务则以非事务方式运行MANDATORY:存在事务则加入,没有事务则抛异常REQUIRES_NEW:创建新事务,存在则挂起当前事务NOT_SUPPORTED:以非事务方式运行,存在事务则挂起当前事务NEVER:以非事务方式运行,存在事务则抛出异常NESTED:当前存在事务则创建一个事务作为当前事务嵌套事务来运行,如果没有事务取值等价于REQUIRED 事务隔离等级 DEAULT:底层数据库默认隔离级别,大部分情况是READ_COMMITEDREAD_UNCOMMITED:表示一个事务可以读取另一个事务修改但没有提交数据,不能防止脏读和不可重复读READ_COMMITED:表示一个事务只能读物另一个事务已经提交的数据,可以防止脏读REPEATABLE_READ:表示一个事务在整个过程中可以多次重复执行某个查询,不管是否中间有新增数据,查询结果都一样,可以防止脏读和重复读SERIALIABLE:所有事务依次执行,可以解决脏读、不可重复读、幻读,但是性能太差; 回滚机制: 如果不做任何处理,只要事务覆盖方法失败都会回滚声明RollbackFor当方法体抛出指定异常类明则执行回滚,其它不回滚声明noRollbackFor当方法体抛出指定异常类明则不执行回滚,其它回滚

    Part-7:高阶应用:自定义Repository实现

    根据EntitryManager进行仓储自定义封装来增强功能,如批量保存、逻辑删除等,代码样例如下:

    @NoRepositoryBean public class BaseDaoImpl implements BaseDao { @PersistenceContext private EntityManager entityManager; @Transactional @Override public <T extends BaseEntity> boolean save(T entity) { this.getSession().save(entity); return true; } private Session getSession() { return entityManager.unwrap(Session.class); } }

    参考资料

    1、Jpa实现假删除:https://my.oschina.net/weechang93/blog/1576594 2、书籍购买点击:《Spring Data JPA从入门到精通》-张振华 3、Druid说明文档:https://github.com/alibaba/druid/wiki

    Processed: 0.016, SQL: 8