Spring Boot 实践折腾记(19):自定义配置JPA使用多数据源

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mickjoust/article/details/80352795

圣爱克苏佩里在《小王子》里写道:用心去看才看得清楚,本质的东西用肉眼是看不见的。

今天的内容有点多,希望你能耐心看完,因为会复习 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,如DataSourceTransactionManagerLocalContainerEntityManagerFactoryBean等。而使用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实现,而直接使用,如CrudRepositoryPagingAndSortingRepositoryJpaRepository等的功能,这些功能提是开箱即用,包含了曹刿操作以及分页和排序等方法。比如,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','mickjoust@gmail.com', false);

insert into mans(id, name, email,disabled)
values(2,'mia','mia@gmail.com', false);

insert into mans(id, name, email,disabled)
values(3,'max','max@gmail.com', true);

这里,由于我们配置了内存数据库(H2),因此,Spring Boot会在启动时自动注册一个DataSource。原理是,Spring Boot autoconfiguration负责创建与LocalContainerEntityManagerFactoryBeanTransactionManager等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.classOrderRepository.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','mickjoust@test.com', false);
insert into mans(id, name, email,disabled)values(2,'mia','mia@test.com', false);
insert into mans(id, name, email,disabled)values(3,'max','max@test.com', true);
delete from orders;
insert into orders(id, cust_name, cust_email)values(1,'hjf','andrew@test.com');
insert into orders(id, cust_name, cust_email)values(2,'tang','paul@test.com');
insert into orders(id, cust_name, cust_email)values(3,'huang','jimmy@test.com');

第九步,在启动类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.

阅读更多

扫码向博主提问

mickjoust

博客专家

非学,无以致疑;非问,无以广识
去开通我的Chat快问
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页