圣爱克苏佩里在《小王子》里写道:用心去看才看得清楚,本质的东西用肉眼是看不见的。
今天的内容有点多,希望你能耐心看完,因为会复习 Spring Boot 和 Sping 的相关内容。
Java持久性API(JPA)是一个对象关系映射(ORM)框架,它是Java EE平台的一部分。 JPA通过让开发人员使用面向对象的API,而不是手动编写SQL查询来简化数据访问层的实现。目前,流行的JPA框架有Hibernate,EclipseLink和OpenJPA。而 Spring 框架本身也提供了一个Spring ORM模块,以便与ORM框架轻松集成。
除此之外,我们还可以在JPA中使用Spring的声明式事务管理功能。除了Spring ORM模块之外,Spring家族还提供了一个Spring Data,主要专注用于访问关系数据库和NoSQL数据库。现在,Spring Data已经集成了大多数流行的数据访问技术,包括JPA,MongoDB,Redis,Cassandra,Solr,ElasticSearch等。
本文我们将探寻Spring Data JPA,并实战演示如何将它与Spring Boot一起使用,以及如何在同一个Spring Boot应用程序中使用多个数据库方法。
Spring Data JPA
在不使用Spring Boot的情况下,我们配置数据存储,通常需要自行配置各种Bean,如DataSource
,TransactionManager
,LocalContainerEntityManagerFactoryBean
等。而使用Spring Boot JPA的Starter:spring-boot-starter-data-jpa
便能快速启动并运行JPA了。但是,在介绍如何使用spring-boot-starter-data-jpa之前,我们先来看看Spring Data JPA。
很多时候,我们可能需要进行数据管理应用程序的开发,对于这些程序,本质上,我们是做的CRUD(创建,读取,更新,删除)操作。过去,我们需要一遍又一遍的写类似的操作代码,也就是我们最为熟悉的DAO层, 而Spring Data的出现,极大的改善了这种情况,我们不再一次又一次地执行相同的CRUD操作,或者每一个项目都需要实现自己的所谓“通用”的CRUD DAO实现,而直接使用,如CrudRepository
,PagingAndSortingRepository
,JpaRepository
等的功能,这些功能提是开箱即用,包含了曹刿操作以及分页和排序等方法。比如,JpaRepository的接口方法,如下图:
如图所示,JpaRepository提供了几种CRUD操作方法:
- long count(); ——返回可用实体的总数;
- boolean existsById(ID id)——返回是否存在具有给定ID的实体;
- List findAll(Sort sort)——返回按给定选项排序的所有实体;
- Page findAll(Pageable pageable)——返回满足Pageable对象中提供的分页限制的实体;
Spring Data JPA不仅提供了开箱即用的CRUD操作,还支持基于方法名称的动态查询生成功能。
比如,定义一个findByEmail(String email)方法,Spring Data将自动生成带有where子句的查询,如“where email =?1”。
再比如,定义一个findByEmailAndPassword(String email,String password)方法,Spring Data 将自动生成带有where子句的查询,如“where email =?1 and password =?2”所示。
但有时候,我们可能因为某些原因(比如,代码安全,编码规范等)而无法直接使用基于方法名称的动态查询,Spring Data 便提供了使用@Query
注释显式配置来增强查询的灵活性。
@Query("select u from User u where u.email=?1 and u.password=?2 and u.enabled=true")
User findByEmailAndPassword(String email, String password);
还可以使用@Modifying和@Query执行数据更新操作,如下所示:
@Modifying
@Query("update User u set u.enabled=:status")
int updateUserStatus(@Param("status") boolean status)
实战1:在Spring Boot中使用Spring Data JPA
现在,我们已经了解了Spring Data JPA是什么以及它提供了哪些功能,本节我们开始实战,数据库选用H2。
第一步,创建一个Spring Boot项目并添加以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
第二步,创建一个JPA实体Man.class
和一个调用JPA的接口ManRepository.class
:
@Entity
@Table(name="MAN")
public class Man {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
@Column(nullable=false)
private String name;
@Column(nullable=false, unique=true)
private String email;
private boolean disabled;
//省略构造函数、get、set
public interface ManRepository extends JpaRepository<Man,Integer>{
}
第三步,创建SQL脚本/src/main/resources/data.sql:
insert into mans(id, name, email,disabled)
values(1,'mickjoust','[email protected]', false);
insert into mans(id, name, email,disabled)
values(2,'mia','[email protected]', false);
insert into mans(id, name, email,disabled)
values(3,'max','[email protected]', true);
这里,由于我们配置了内存数据库(H2),因此,Spring Boot会在启动时自动注册一个DataSource。原理是,Spring Boot autoconfiguration
负责创建与LocalContainerEntityManagerFactoryBean
,TransactionManager
等JPA相关的bean,默认创建。
第四步,创建一个启动类:
@SpringBootApplication
@RestController
public class ManController {
@Autowired
private ManRepository manRepository;
public static void main(String[] args) {
SpringApplication.run(ManController.class,args);
}
@GetMapping("/mans")
public List<Man> getAllMans(){
return manRepository.findAll();
}
}
完成,启动,访问 http://localhost:8080/mans,获取全部用户数据。
实战1的补充
如果想要使用动态SQL,则可以像下面这样写:
@Query("select u from User u where u.name like %?1%")
List<User> searchByName(String name)
我们还可以使用分类和分页功能(对于一个web应用,反复写分页其实很无奈),这里,我们假设想按照升序排列所有用户的名字,我们可以使用findAll(Sort sort)方法,如下:
Sort sort = new Sort(Sort.Direction.ASC,"name");
List<Man> mans = manRepository.findAll(sort);
//还可以对多个属性应用排序,如下所示:
Sort.Order order1 =new Sort.Order(Sort.Direction.ASC,"name");
Sort.Order order2 =new Sort.Order(Sort.Direction.DESC,"id");
Sort sort1 = Sort.by(order1,order2);
List<Man> users = manRepository.findAll(sort1);
代码中,用户首先按名称排序,然后按id降序排列。
然后,假设我们想在一个页面上加载前25个用户。这是就可以使用Pageable和PageRequest按页面获取结果,如下所示:
int size = 25;
int page = 0; //基于零的页面索引。
Pageable pageable = PageRequest.of(page,size);
Page<Man> usersPage = manRepository.findAll(pageable);
usersPage将只包含前25个用户记录。 我们还可以获得更多详细信息,例如总页数,当前页码,是否存在下一页,是否存在上一页等等(而这在过去需要写很多代码的)。
usersPage.getTotalElements(); - 返回元素的总数量。
usersPage.getTotalPages(); - 返回总页数。
usersPage.hasNext();
usersPage.hasPrevious();
List<Man> usersList = usersPage.getContent();
还可以按如下方式应用分页和排序:
Sort sort2 = new Sort(Sort.Direction.ASC,"name");
Pageable pageable1 = PageRequest.of(page,size,sort2);
Page<Man> usersPage1 = manRepository.findAll(pageable1);
实战2:自定义使用多个数据库
通常情况下,如果我们只连一个数据库,那么通过Spring Boot可以很快的自动配置。但是,在真实的生产环境中,我们可能需要配置多个数据库(虽然按照微服务的理念,不推荐这样做),但我们还是想要实现这样的功能,如果直接使用spring-boot-starter-data-jpa定义数据源bean,则Spring Boot将会尝试自动创建一些bean(例如TransactionManager),而我们的目标是多个数据源,这样直接配置它会失败,办法是——需要关闭特定的自动配置并自行配置组件。
同样,我们先假设一个场景,其中用户数据存储在一个数据库中,而与订单相关的数据存储在另一个数据库/模式中。
接下来,我们一步一步的实战。
第一步,创建一个Spring Boot应用程序。在pom.xml中配置以下mysql数据库的依赖项:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
第二步,创建一个启动类,并关闭Spring Boot下DataSource 的JPA自动配置。 由于我们要明确配置与数据库相关的bean,因此需要通过排除AutoConfiguration类来关闭自动配置,如下所示
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class})
@EnableTransactionManagement
public class ManControllerExclude {
public static void main(String[] args) {
SpringApplication.run(ManControllerExclude.class,args);
}
}
注意,由于我们关闭了AutoConfigurations里的事务配置,因此需要使用@EnableTransactionManagement来显式启用TransactionManagement注解。
第三步,在配置文件中加入自定义数据源属性。 我们可以在application.properties文件中配置叫datasource.security.和datasource.orders.的数据源配置。
datasource.mans.driver-class-name=com.mysql.jdbc.Driver
datasource.mans.url=jdbc:mysql://localhost:3306/mans
datasource.mans.username=root
datasource.mans.password=hjf123456
datasource.man.initialize=true
datasource.orders.driver-class-name=com.mysql.jdbc.Driver
datasource.orders.url=jdbc:mysql://localhost:3306/orders
datasource.orders.username=root
datasource.orders.password=hjf123456
datasource.orders.initialize=true
hibernate.hbm2ddl.auto=update
hibernate.show-sql=true
在这里,我们已经使用自定义属性键来配置了两个数据源属性,但还没有生效。
第四步,定义自定义的用户的datasource前缀数据源(这是Spring Boot的一大特色),使上面的配置能生效,如下代码:
@Configuration
@EnableJpaRepositories( //0
basePackages = "com.hjf.boot.demo.database.jpa",
entityManagerFactoryRef = "manEntityManagerFactory",
transactionManagerRef = "manTransactionManager"
)
public class ManDBConfig {
@Autowired
private Environment env; //1
@Bean
@ConfigurationProperties(prefix="datasource.man")
public DataSourceProperties manDataSourceProperties() { //2
return new DataSourceProperties();
}
@Bean
public DataSource manDataSource() { //3
DataSourceProperties manDataSourceProperties = manDataSourceProperties();
return DataSourceBuilder.create()
.driverClassName(manDataSourceProperties.getDriverClassName())
.url(manDataSourceProperties.getUrl())
.username(manDataSourceProperties.getUsername())
.password(manDataSourceProperties.getPassword())
.build();
}
@Bean
public PlatformTransactionManager manTransactionManager() { //4
EntityManagerFactory factory = manEntityManagerFactory().getObject();
return new JpaTransactionManager(factory);
}
@Bean
public LocalContainerEntityManagerFactoryBean manEntityManagerFactory() {//5
LocalContainerEntityManagerFactoryBean factory =
new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(manDataSource());
factory.setPackagesToScan("com.hjf.boot.demo.database.jpa");
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
jpaProperties.put("hibernate.show-sql", env.getProperty("hibernate.show-sql"));
factory.setJpaProperties(jpaProperties);
return factory;
}
@Bean
public DataSourceInitializer manDataSourceInitializer() {//6
DataSourceInitializer dsInitializer = new DataSourceInitializer();
dsInitializer.setDataSource(manDataSource());
ResourceDatabasePopulator dbPopulator = new ResourceDatabasePopulator();
dbPopulator.addScript(new ClassPathResource("security-data.sql"));
dsInitializer.setDatabasePopulator(dbPopulator);
dsInitializer.setEnabled(env.getProperty("datasource.man.initialize",
Boolean.class, false) );
return dsInitializer;
}
}
代码说明:
0——注解@EnableJpaRepositories
的作用是开启Jpa的支持,因为我们会有多个自定义JPA,就需要单独实现各自的管理类,其中,entityManagerFactoryRef
是实体关联管理工厂类和transactionManagerRef
事务管理类,都需要我们自行实现。
1——Spring 3中引入了属性管理类,这里主要用来引入一些系统环境属性。
2——定义前缀,并返回DataSourceProperties
对象,为什么?这是因为自动配置时加载的对象DataSourceAutoConfiguration
会定义它,并通过它读取配置的内容,这里new个新对象就行了,主要是为后面的bean初始化服务。
3——获取前缀的DataSourceProperties对象,并创建真正的DataSource
数据源,这里我们使用的是Spring Boot自带的工具类DataSourceBuilder
,值来源的就是从前缀对象中读取的值,换句话说,就是在配置文件里我们写的值;
4——事物管理器的主接口PlatformTransactionManager
需要获取到JpaTransactionManager
的对象进行事务管理,这个对象就是由下面//5的工厂方法创建的。
5——这是JPA的开发规范,的确很麻烦,这是为了简单而做出的牺牲,简单说,这里会将我们的数据源,适配器,配置策略都全部封装好返回,让//4调用来创建返回给事务管理器。而同时,Boot会通过加载配置属性来获取配置值。
6——是否进行DataSource初始化,如果设置,就会调用这里的方法执行对应的sql文件。
第六步,同样,按照第五步的方法,定义另一JPA数据
@Configuration
@EnableJpaRepositories(
basePackages = "com.hjf.boot.demo.database.jpa",
entityManagerFactoryRef = "ordersEntityManagerFactory",
transactionManagerRef = "ordersTransactionManager"
)
public class OrdersDBConfig {
@Autowired
private Environment env;
@Bean
@ConfigurationProperties(prefix="datasource.orders")
public DataSourceProperties ordersDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource ordersDataSource() {
DataSourceProperties primaryDataSourceProperties = ordersDataSourceProperties();
return DataSourceBuilder.create()
.driverClassName(primaryDataSourceProperties.getDriverClassName())
.url(primaryDataSourceProperties.getUrl())
.username(primaryDataSourceProperties.getUsername())
.password(primaryDataSourceProperties.getPassword())
.build();
}
@Bean
public PlatformTransactionManager ordersTransactionManager() {
EntityManagerFactory factory = ordersEntityManagerFactory().getObject();
return new JpaTransactionManager(factory);
}
@Bean
public LocalContainerEntityManagerFactoryBean ordersEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new
LocalContainerEntityManagerFactoryBean();
factory.setDataSource(ordersDataSource());
factory.setPackagesToScan("com.hjf.boot.demo.database.jpa");
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.hbm2ddl.auto",env.getProperty("hibernate.hbm2ddl.auto"));
jpaProperties.put("hibernate.show-sql", env.getProperty("hibernate.show-sql"));
factory.setJpaProperties(jpaProperties);
return factory;
}
@Bean
public DataSourceInitializer ordersDataSourceInitializer() {
DataSourceInitializer dsInitializer = new DataSourceInitializer();
dsInitializer.setDataSource(ordersDataSource());
ResourceDatabasePopulator dbPopulator = new ResourceDatabasePopulator();
dbPopulator.addScript(new ClassPathResource("orders-data.sql"));
dsInitializer.setDatabasePopulator(dbPopulator);
dsInitializer.setEnabled(env.getProperty("datasource.orders.initialize",
Boolean.class, false));
return dsInitializer;
}
}
第六步,创建一个man相关的JPA实体和一个订单的JPA实体,如下所示:
@Entity
@Table(name="MAN")
public class Man {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
@Column(nullable=false)
private String name;
@Column(nullable=false, unique=true)
private String email;
private boolean disabled;
//省略构造函数、get、set
@Entity
@Table(name="ORDERS")
public class Order {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
@Column(nullable=false, name="cust_name")
private String customerName;
@Column(nullable=false, name="cust_email")
private String customerEmail;
//省略构造函数、get、set
}
第七步,创建Repository服务,包括ManRepository.class
和OrderRepository.class
,如下:
... //ManRepository是使用前一小节的代码,这里省略
public interface OrderRepository extends JpaRepository<Order,Integer> {
}
第八步,创建SQL脚本并初始化示例数据。 在src / main / resources下创建man-data.sql脚本和orders-data.sql脚本,如下
delete from mans;
insert into mans(id, name, email,disabled)values(1,'mickjoust','[email protected]', false);
insert into mans(id, name, email,disabled)values(2,'mia','[email protected]', false);
insert into mans(id, name, email,disabled)values(3,'max','[email protected]', true);
delete from orders;
insert into orders(id, cust_name, cust_email)values(1,'hjf','[email protected]');
insert into orders(id, cust_name, cust_email)values(2,'tang','[email protected]');
insert into orders(id, cust_name, cust_email)values(3,'huang','[email protected]');
第九步,在启动类ManControllerExclude中补充查询服务,如下:
...
@Autowired
private ManRepository manRepository;
@Autowired
private OrderRepository orderRepository;
@GetMapping("/man")
public List<Man> getAllByMan(){
return manRepository.findAll();
}
@GetMapping("/order")
public List<Order> getAllByOrder(){
return orderRepository.findAll();
}
到此,完成,访问http://localhost:8080/mans或…/order,就能够看到数据库的数据了。
示例地址:boot-database/jpa
小结
多说一句,很多同学一看代码多、文字多就头痛,认为自定义这么麻烦,那还不如就用自动配置就行了,事实上,只要能搞清楚底层的思维原理,定制化的自定义是很方便的(至少比写汇编好多了吧),换句话说,就是按照定义的约束来实现就行了,关于自定义配置,还可以参看在文章《》。真实的场景一定是复杂多变的,如果只是别人提供了自己觉得就该这么用,不搞清楚背后的原理,很容易掉进新的坑里。
本文介绍了如何在Spring Boot中使用Spring Data JPA,并通过一个例子详细介绍了该如何自定多个JPA的数据源, 希望能对你有所启发。
参考资源
1、Spring Boot 官方文档
2、Spring Data Jpa官方文档:http://docs.spring.io/spring-data/jpa/docs/current/reference/html。
3、JPA查询方法详解:http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.