上一章介绍了spring boot 集成单元测试,这样spring boot 基础的部分就完成了。
这一章就聊聊在Spring boot 下用JPA 进行数据访问. Spring Data JPA 是JPA规范的一个轻量级实现. 相信大多数人在spring 时期就已经了解或者使用过Spring data jpa了. 下面先简单说下spring boot 中如何配置和使用spring data jpa.
1.先新建一个module. 在module的pom文件中引入下面这些依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.28</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2. 按照之前章节的方法构建一个spring boot的启动入口类, 代码如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.需要在yml配置文件中配置Spring data jpa和数据源信息.配置文件如下:
下面这是druid数据源的配置, 这个没什么说的.
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
#连接池的配置信息
initialSize: 10
minIdle: 10
maxActive: 100
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
下面是jpa的配置
jpa:
show-sql: false
generate-ddl: true
hibernate.ddl-auto: update # Hibernate ddl auto (create, create-drop, update,none)
database-platform: org.hibernate.dialect.MySQL5Dialect
这些配置并不能完成spring data jpa的加载, 我们还需要分别定义druid和spring data jpa的config类
4.在写config类之前,我们先说下spring 的@Configuration和@Bean
Spring 时代,我们要写大量的xml来完成项目资源的加载. 到了Spring boot 时代, 就提倡零配置了, 其实就是换成java文件来完成配置 @Configuration 需要在类上添加, 它的作用相当于xml配置文件中的beans标签.一个配置类就相当于以前的一个配置文件. @Bean加在方法上, 相当于xml中的bean标签. 一个@Configuration标注的类,可以有多个@Bean标注的方法.
介绍完@Configuration和@Bean, 接下来先看看druid的配置类, 代码如下:
@Configuration
public class DruidDBConfig {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.testOnReturn}")
private boolean testOnReturn;
@Value("${spring.datasource.poolPreparedStatements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
private int maxPoolPreparedStatementPerConnectionSize;
@Value("{spring.datasource.connectionProperties}")
private String connectionProperties;
@Bean(name="dataSource") // 声明其为Bean实例
@Primary // 在同样的DataSource中,首先使用被标注的DataSource
public DataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
// configuration
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
datasource.setConnectionProperties(connectionProperties);
return datasource;
}
}
注: @Primary这个注解表示标注的bean被优先使用, 当我们配置多数据源时, 被这个标签标记的数据源优先使用.
这个类其实很好理解, 就是把通过@Value加载到yml配置的值, 注入到 dataSource这个bean的property里. 因为有@Configuration的存在, spring boot会在启动是自动加载dataSource这个bean.
5.再添加一个spring data jpa的配置类, 代码如下:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = {"org.learning.repository"},
repositoryFactoryBeanClass = JpaRepositoryFactoryBean.class)
public class RepositoryConfig {
private static final String HIBERNATE_DIALECT = "hibernate.dialect";
private static final String HIBERNATE_SHOW_SQL = "hibernate.show.sql";
private static final String HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
private static final String HIBERNATE_EJB_NAMING_STRATEGY = "hibernate.ejb.naming_strategy";
@Autowired
private DataSource dataSource;
@Value("${spring.jpa.show-sql}")
private String showSql;
@Value("${spring.jpa.generate-ddl}")
private String generateDdl;
@Value("${spring.jpa.hibernate.ddl-auto}")
private String hibernateDdl;
@Value("${spring.jpa.database-platform}")
private String databasePlatform;
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setDataSource(dataSource);
factory.setPackagesToScan("org.learning.domain");
Map<String, Object> jpaProperties = new HashMap<>();
jpaProperties.put(HIBERNATE_SHOW_SQL, showSql);
jpaProperties.put(HIBERNATE_DIALECT, databasePlatform);
jpaProperties.put(HIBERNATE_HBM2DDL_AUTO, hibernateDdl);
jpaProperties.put(HIBERNATE_EJB_NAMING_STRATEGY, "org.hibernate.cfg.ImprovedNamingStrategy");
factory.setJpaPropertyMap(jpaProperties);
return factory;
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory().getObject());
return txManager;
}
}
先看看标注在类上的注解
- @EnableTransactionManagement 这个注解表示我们需要开启事务管理
- @EnableJpaRepositories 这个注解表示需要开启jpa Repositories . 这个注解中的basePackages 指定了repository存在的路径 . repositoryFactoryBeanClass 指定了创建repository实例的工厂类
接下来看看类中的两个Bean
- entityManagerFactory 这个bean 定义了 spring data jpa 的 EntityManagerFactoryBean . 这和以前在spring 下xml定义的方式基本一样的.
- transactionManager 这里定义了jpa的事务.
好了, 基于spring data 的数据访问层就定义好了.
6.剩下的工作就是建立domain, repository 和service了, 废话不多说, 直接上代码
一个名叫User的domain(这里使用了lombok减少代码, 如对lombok不太了解的可以看看我写的lombok介绍和配置 传送门 ):
@Data
@Entity
@Table(name="lesson_user")
public class User implements Serializable{
/** ID */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 用户名 */
@Column(length = 50)
@NotNull
private String userName;
/** 密码 */
@Column(length = 20)
@NotNull
private String password;
/** 手机号 */
@Column(length = 15)
private String tel;
/** 性别 */
@Column
@Enumerated(value = EnumType.STRING)
private UserSex userSex;
}
再写一个 User里使用的枚举UserSex:
public enum UserSex {
MAN, WOMAN
}
7.Domain相关的类建立好了, 这部分没什么可说的,照常写就好了. 下面写个User对应的Repository.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
/**
* 根据用户名查找
* @param userName 用户名
* @return User
*/
User findByUserName(String userName);
/**
* 根据手机号和用户名查找
* @param tel 手机号
* @param userName 用户名
* @return User
*/
User findByTelAndUserName(String tel, String userName);
/**
* 根据手机号查询用户
* @param tel 手机号
* @return 用户
*/
@Query("FROM User WHERE tel=:tel")
User findByTel(@Param("tel")String tel);
/**
* 分页查询
* @param pageable
* @return
*/
@Query(value = "SELECT * FROM lesson_user",
countQuery = "SELECT count(*) FROM lesson_user",
nativeQuery = true)
Page<User> pageAll(Pageable pageable);
}
Spring Data JPA的Repository需要注意以下几点:
- 必须是个接口, 且需要继承JpaRepository或者JpaRepository的子类(后面章节会介绍自定义JpaRepository)
- 继承JpaRepository后泛型必须指定对应的domain和domain的ID的类型,我们这里的ID使用的Long型.
- 类上必须注解@Repository.
- Repository的方法中如果以findBy前缀, 后面跟domain的列名时,表示用这个列为查询条件. 多个列以And分割, 参数需要和查询的列保持一致
- 可以在方法中使用@Query标注Hql进行数据库操作, 入参":"表示, 方法入参需要标记@Param. 修改和删除操作需要在@Query之外添加@Modifying标记, 表示这是对数据库进行修改操作的.
- 想要执行分页查询可以依照例子中最后一个方法这样去写.
- 由于当前这个例子继承了JpaRepository, 所以这个UserRepository就具有了JpaRepository的所有方法, JpaRepository的默认方法如下:
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}
8.接下来, 创建Service的接口和实现, 在Service中会有使用上面JpaRepository这些默认方法的例子.代码如下:
UserService 接口:
public interface UserService {
/**
* 保存一个用户
* @param user
*/
User save(User user);
/**
* 根据用户名查找
* @param userName 用户名
* @return User
*/
User findByUserName(String userName);
/**
* 根据手机号和用户名查找
* @param tel 手机号
* @param userName 用户名
* @return User
*/
User findByTelAndUserName(String tel, String userName);
/**
* 根据手机号查询用户
* @param tel 手机号
* @return 用户
*/
User findByTel(String tel);
/**
* 查询列表
* @return 一组user
*/
List<User> list();
/**
* 查找列表并按照name排序
* @return
*/
List<User> listAndOrderByName();
/**
* 分页查询
* @return
*/
Page<User> page(Pageable pageable);
/**
* 清空表
*/
void deleteAll();
}
UserServiceImpl实现:
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional(rollbackFor = Exception.class)
public User save(User user) {
return userRepository.save(user);
}
@Override
@Transactional(readOnly = true)
public User findByUserName(String userName) {
return userRepository.findByUserName(userName);
}
@Override
@Transactional(readOnly = true)
public User findByTelAndUserName(String tel, String userName) {
return userRepository.findByTelAndUserName(tel, userName);
}
@Override
@Transactional(readOnly = true)
public User findByTel(String tel) {
return userRepository.findByTel(tel);
}
@Override
@Transactional(readOnly = true)
public List<User> list() {
return userRepository.findAll();
}
@Override
@Transactional(readOnly = true)
public List<User> listAndOrderByName() {
return userRepository.findAll(new Sort(Sort.Direction.DESC, "id"));
}
@Override
@Transactional(readOnly = true)
public Page<User> page(Pageable pageable) {
return userRepository.pageAll(pageable);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteAll() {
userRepository.deleteAll();
}
}
注:service实现中不要忘记添加事务@Transactional注解, 查询设置为readOnly = true
9.最后, 我们通过一个JunitTest类测试一下这些例子:
@Slf4j
public class UserServiceTest extends BaseTest {
@Autowired
private UserService userService;
@Before
public void beforeTest() {
log.info("UserServiceTest前置方法");
User user1 = new User();
user1.setTel("12345678");
user1.setUserName("测试1");
user1.setPassword("123456");
user1.setUserSex(UserSex.MAN);
userService.save(user1);
User user2 = new User();
user2.setTel("22345678");
user2.setUserName("测试2");
user2.setPassword("123456");
user2.setUserSex(UserSex.WOMAN);
userService.save(user2);
}
@After
public void testAfterTest() {
log.info("UserServiceTest后置方法");
userService.deleteAll();
}
@Test
public void testFindByUserName(){
User user = userService.findByUserName("测试1");
log.info("查询到的用户{}", JSONObject.toJSONString(user));
Assert.notNull(user, "无效的用户名");
}
@Test
public void testFindByTelAndUserName(){
User user = userService.findByTelAndUserName("12345678", "测试1");
log.info("查询到的用户{}", JSONObject.toJSONString(user));
Assert.notNull(user, "无效的手机号或者用户名");
}
@Test
public void testFindByTel(){
User user = userService.findByTel("12345678");
log.info("查询到的用户{}", JSONObject.toJSONString(user));
Assert.notNull(user, "无效的手机号");
}
@Test
public void testList(){
List<User> userList = userService.list();
log.info("查询到的用户{}", JSONObject.toJSONString(userList));
Assert.notNull(userList, "空记录");
}
@Test
public void testPage(){
Page<User> page = userService.page(PageRequest.of(0,10));
log.info("查询到的用户{}", JSONObject.toJSONString(page));
Assert.notNull(page, "空记录");
}
@Test
public void testListAndOrderByName(){
List<User> userList = userService.listAndOrderByName();
log.info("查询到的用户{}", JSONObject.toJSONString(userList));
Assert.notNull(userList, "空记录");
}
}
大家可以跑下最后的单元测试,看看是不是都能测试通过.
这一章, 介绍了Spring data jpa与spring boot的整合, 还有Spring data jpa的一些简单使用, 下一章介绍JAP在实际开发中的封装和应用.
本章结束
下一章介绍一个封装扩展spring data jpa的例子