整合 Spring Data JPA

JPA (Java Persistence API)和Spring Data是两个范畴的概念。

作为一名Java EE工程师,基本都有听说过Hibernate 框架。Hibernate 是一-一个ORM框架,而JPA则是一种ORM规范,JPA和Hibernate的关系就像JDBC与JDBC驱动的关系,即JPA制定了ORM规范,而Hibernate是这些规范的实现(事实上,是先有Hibernate 后有JPA,JPA规范的起草者也是Hibernate的作者),因此从功能上来说,JPA相当于Hibernate 的一个子集。

Spring Data是Spring的一个子项目,致力于简化数据库访问,通过规范的方法名称来分析开发者的意图,进而减少数据库访问层的代码量。Spring Data 不仅支持关系型数据库,也支持非关系型数据库。Spring Data JPA可以有效简化关系型数据库访问代码。

Spring Boot整合Spring Data JPA的步骤如下。

1. 创建数据库

注意:创建数据库即可,不用创建表。

2. 创建项目

创建Spring Boot项目,添加MySQL和Spring Data JPA的依赖,代码如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

3. 数据库配置

在application.properties中配置数据库基本信息以及JPA相关配置:

spring:
  datasource:
    username: admin
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://120.55.61.170:3306/suohechuan?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  jpa:
    show-sql: true
    database: mysql
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL57Dialect

这里的配置信息主要分为两大类,第18行是数据库基本信息配置,第916行是JPA相关配置。其中,第10行表示是否在控制台打印JPA执行过程生成的SQL,第11行表示JPA对应的数据库是MySQL,第13行表示在项目启动时根据实体类更新数据库中的表(其他可选值有create、create-drop、validate、 no),第16行则表示使用的数据库方言是MySQL57Dialect。

4. 创建实体类

创建Book实体类,代码如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity(name="t_book")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name="book_name",nullable = false)
    private String name;
    private String author;
    private Float price;
    @Transient
    private String description;
}

代码解释:

  • @Entity 注解表示该类是一个实体类,在项目启动时会根据该类自动生成一张表,表的名称即@Entity注解中name的值,如果不配置name,默认表名为类名。所有的实体类都要有主键,@Id注解表示该属性是一一个主键,@GeneratedValue 注解表示主键自动生成,strategy 则表示主键的生成策略。
  • 默认情况下,生成的表中字段的名称就是实体类中属性的名称,通过@Column注解可以定制生成的字段的属性,name 表示该属性对应的数据表中字段的名称,nullable 表示该字段非空。
  • @Transient 注解表示在生成数据库中的表时,该属性被忽略,即不生成对应的字段。

5. 创建BookDao接口

创建BookDao接口,继承JpaRepository,代码如下:

public interface BookDao extends JpaRepository<Book, Integer> {
    List<Book> getBooksByAuthorStartingwith(String author);

    List<Book> getBooksByPriceGreaterThan(Float price);

    @Query(value = "select * from t_book where id=(select max (id) from t_book) ", nativeQuery = true)
    Book getMaxIdBook();

    @Query("select b from t_book b where b.id>:id and b.author=:author")
    List<Book> getBookByIdAndAuthor(@Param("author") String author, @Param("id") Integer id);

    @Query("select b from t_book b where b.id<?2 and b.name like %?1%")
    List<Book> getBooksByIdAndName(String name, Integer id);
}

代码解释:

  • 自定义BookDao继承自JpaRepository。JpaRepository中提供了一些基本的数据操作方法,有基本的增删改查、分页查询、排序查询等。
  • 第2行定义的方法表示查询以某个字符开始的所有书。
  • 第3行定义的方法表示查询单价大于某个值的所有书。
  • 在Spring Data JPA中,只要方法的定义符合既定规范,Spring Data就能分析出开发者的意图,从而避免开发者定义SQL。所谓的既定规范,就是一定的方法命名规则。支持的命名规则如表所示。

KeyWords

方法命名举例

方法命名举例

And

findByNameAndAge

where name= ? and age =?

Or

findByNameOrAge

where name= ? or age=?

Is

findByAgels

where age= ?

Equals

findByldEquals

where id= ?

Between

findByAgeBetween

where age between ? and ?

LessThan

findByAgeLessThan

where age <?

LessThanEquals

findByAgeLessThanEquals

where age<=?

GreaterThan

findByAgeGreaterThan

where age > ?

GreaterThanEquals

findByAgeGreater ThanEquals

where age > = ?

After

findByAgeAfter

where age > ?

Before

findByAgeBefore

where age < ?

IsNull

findByNameIsNull

where name is null

isNotNull,NotNull

findByNameNotNull

where name is not null

Not

findByGenderNot

where gender <>?

In

findByAgeIn

where age in (?)

NotIn

findByAgeNotIn

where age not in (?)

NotLike

findByNameNotLike

where name not like ?

Like

findByNameLike

where name like ?

StartingWith

findByNameStartingWith

where name like ‘?%’

EndingWith

findByNameEndingWith

where name like ‘%?’

Containing,Contains

findByNameContaining

where name like ‘%?%’

OrderBy

findByAgeGreater ThanOrderByldDesc

where age>? order by id desc

True

findByEnabledTue

where enabled= true

False

findByEnabledFalse

where enabled= false

IgnoreCase

findByNameIgnoreCase

where UPPER(name)=UPPER(?)

  • 既定的方法命名规则不一定满足所有的开发需求,因此Spring Data JPA也支持自定义JPQL( Java Persistence Query Language)或者原生SQL。第6~7行表示查询id最大的书, nativeQuery= true表示使用原生的SQL查询。
  • 第9~10行表示根据 id和author进行查询,这里使用默认的JPQL语句。JPQL是一种面向对象表达式语言,可以将SQL语法和简单查询语义绑定在一起,使用这种语言编写的查询是可移植的,可以被编译成所有主流数据库服务器上的SQL。JPQL与原生SQL语句类似,并且完全面向对象,通过类名和属性访问,而不是表名和表的属性(用过Hibernate的读者会觉得这类似于HQL)。第9~10 行的查询使用:id、:name这种方式来进行参数绑定。注意:这里使用的列名是属性的名称而不是数据库中列的名称。
  • 第12、13行也是自定义JPQL查询,不同的是传参方式使用?1、?2这种方式。注意:方法中参数的顺序要与参数声明的顺序一致。
  • 如果BookDao中的方法涉及修改操作,就需要添加@Modifying注解并添加事务。

6. 创建BookService

创建BookService,代码如下:

@Service
public class BookService {
    @Autowired
    BookDao bookDao;

    public void addBook(Book book) {
        bookDao.save(book);
    }

    public Page<Book> getBookByPage(Pageable pageable) {
        return bookDao.findAll(pageable);
    }

    public List<Book> getBooksByAuthorStartingWith(String author) {
        return bookDao.getBooksByAuthorStartingWith(author);
    }

    public List<Book> getBooksByPriceGreaterThan(Float price) {
        return bookDao.getBooksByPriceGreaterThan(price);
    }

    public Book getMaxIdBook() {
        return bookDao.getMaxIdBook();
    }

    public List<Book> getBookByIdAndAuthor(String author,Integer id) {
        return bookDao.getBookByIdAndAuthor(author, id);
    }

    public List<Book> getBooksByIdAndName(String name,Integer id) {
        return bookDao.getBooksByIdAndName(name, id);
    }
}

代码解释:

  • 第7行使用save方法将对象数据保存到数据库,save方法是由JpaRepository接口提供的。
  • 第10行是一个分页查询,使用findAll 方法,返回值为Page,该对象中包含有分页常用数据,例如总记录数、总页数、每页记录数、当前页记录数等。

7. 创建BookController

创建BookController,实现对数据的测试:

@RestController
public class BookController {
    @Autowired
    BookService bookService;

    @GetMapping("/findAll")

    public void findAll() {
        PageRequest pageable = PageRequest.of(2, 3);
        Page<Book> page = bookService.getBookByPage(pageable);
        System.out.println("总页数:" + page.getTotalPages());
        System.out.println("总记录数:" + page.getTotalElements());
        System.out.println("查询结果:" + page.getContent());
        System.out.println("当前页数:" + (page.getNumber() + 1));
        System.out.println("当前页记录数: " + page.getNumberOfElements());
        System.out.println("每页记录数: " + page.getSize());
    }

    @GetMapping("/search")
    public void search() {
        List<Book> bs1 = bookService.getBookByIdAndAuthor("鲁迅", 7);
        List<Book> bs2 = bookService.getBooksByAuthorStartingWith("吴");
        List<Book> bs3 = bookService.getBooksByIdAndName("西", 8);
        List<Book> bs4 = bookService.getBooksByPriceGreaterThan(30F);
        Book b = bookService.getMaxIdBook();
        System.out.println("bs1 : " + bs1);
        System.out.println("bs2 : " + bs2);
        System.out.println("bs3 : " + bs3);
        System.out.println("bs4 : " + bs4);
        System.out.println("b : " + b);
    }

    @GetMapping("/save")
    public void save() {
        Book book = new Book();
        book.setAuthor("鲁迅");
        book.setName("呐喊");
        book.setPrice(23F);
        bookService.addBook(book);
    }
}

代码解释:

  • 在findAll接口中,首先通过调用PageRequest中的of方法构造PageRequest对象。of方法接收两个参数:第一个参数是页数,从0开始计;第二个参数是每页显示的条数。
  • 在save接口中构造一个Book对象,直接调用save方法保存起来即可。

8.测试

最后调用相关接口进行测试,数据库中的测试数据sql如下。

INSERT INTO `t_book` VALUES (1, '罗贯中', '三国演义', 30);
INSERT INTO `t_book` VALUES (2, '曹雪芹', '红楼梦', 35);
INSERT INTO `t_book` VALUES (3, '吴承恩', '西游记', 29);
INSERT INTO `t_book` VALUES (4, '施耐庵', '水浒传', 29);
INSERT INTO `t_book` VALUES (5, '钱钟书', '宋诗选注', 33);
INSERT INTO `t_book` VALUES (6, '鲁迅', '朝花夕拾', 18);
INSERT INTO `t_book` VALUES (7, '鲁迅', '故事新选', 22);

id

author

book_name

price

1

罗贯中

三国演义

30

2

曹雪芹

红楼梦

35

3

吴承恩

西游记

29

4

施耐庵

水浒传

29

5

钱钟书

宋诗精选

33

6

鲁迅

朝花夕拾

18

7

鲁迅

故事新选

22

首先调用http://localhost:8080/findAll 接口,控制台打印日志。

总页数:3
总记录数:7
查询结果:[Book(id=7, name=故事新选, author=鲁迅, price=22.0, description=null)]
当前页数:3
当前页记录数: 1
每页记录数: 3

接下来调用http://localhost:8080/save 接口,调用后数据库中的数据如图所示。

id

author

book_name

price

1

罗贯中

三国演义

30

2

曹雪芹

红楼梦

35

3

吴承恩

西游记

29

4

施耐庵

水浒传

29

5

钱钟书

宋诗精选

33

6

鲁迅

朝花夕拾

18

7

鲁迅

故事新选

22

8

鲁迅

呐喊

23

最后调用http://localhost:8080/search接口,控制台打印日志。

bs1 : [Book(id=8, name=呐喊, author=鲁迅, price=23.0, description=null)]
bs2 : [Book(id=3, name=西游记, author=吴承恩, price=29.0, description=null)]
bs3 : [Book(id=3, name=西游记, author=吴承恩, price=29.0, description=null)]
bs4 : [Book(id=2, name=红楼梦, author=曹雪芹, price=35.0, description=null), Book(id=5, name=宋诗选注, author=钱钟书, price=33.0, description=null)]
b : Book(id=8, name=呐喊, author=鲁迅, price=23.0, description=null)

JPA多数据源

JPA 和 MyBatis配置多数据源类似,不同的是,JPA配置时主要提供不同的LocalContainerEntityManagerFactoryBean以及事务管理器,具体配置步骤如下。

1. 创建数据库

注意:创建数据库即可,不用创建表。

2. 创建项目

创建Spring Boot Web项目,添加如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

注意这里添加的数据库连接池依赖是druid-spring-boot-starter。 druid-spring-boot-starter 可以帮助开发者在SpringBoot项目中轻松集成Druid数据库连接池和监控。

3. 配置数据库连接

在application.properties中配置数据库连接信息,代码如下:

spring:
  datasource:
    #数据源1
    one:
      username: admin
      password: 123456
      #?serverTimezone=UTC解决时区的报错
      url: jdbc:mysql://120.55.61.170:3306/suohechuan?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-class-name: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
    #数据源2
    two:
      username: fristweb
      password: dTNFJW4B5MrwT4KS
      #?serverTimezone=UTC解决时区的报错
      url: jdbc:mysql://120.55.61.170:3306/fristweb?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-class-name: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
  jpa:
    properties:
      show-sql: true
      database: mysql
      hibernate:
        hbm2ddl:
          auto: update
        dialect: org.hibernate.dialect.MySQL57Dialect

注意:这里的配置与配置单独的JPA有区别,因为在后文的配置中要从JpaProperties中的getProperties方法中获取所有JPA相关的配置,因此这里的属性前缀都是spring.jpa.properties。

4. 配置数据源

创建DataSourceConfig配置数据源,根据application.properties中的配置生成两个数据源:

@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.one")
    DataSource dsOne(){
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.two")
    DataSource dsTwo(){
        return DruidDataSourceBuilder.create().build();
    }
}

代码解释:

  • DataSourceConfig中提供了两个数据源: dsOne 和dsTwo,默认方法名即实例名。
  • @ConfigurationProperties 注解表示使用不同前缀的配置文件来创建不同的DataSource实例。

5. 创建实体类

在 model包下创建实体类User,代码如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity(name = "t_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String gender;
    private Integer age;
}

根据实体类在数据库中创建t_user表,表中的id字段自增长。

6. 创建JPA配置

接下来是核心配置,根据两个配置好的数据源创建两个不同的JPA配置,代码如下:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.example.demo.dao1", entityManagerFactoryRef = "entityManagerFactoryBeanOne", transactionManagerRef = "platformTransactionManagerOne")
public class JpaConfigOne {
    @Resource(name = "dsOne")
    DataSource dsOne;
    @Autowired
    JpaProperties jpaProperties;

    @Bean
    @Primary
    LocalContainerEntityManagerFactoryBean entityManagerFactoryBeanOne(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dsOne)
                .properties(jpaProperties.getProperties())
                .packages("com.example.demo.model")
                .persistenceUnit("pu1")
                .build();
    }

    @Bean
    PlatformTransactionManager platformTransactionManagerOne(
            EntityManagerFactoryBuilder builder) {
        LocalContainerEntityManagerFactoryBean factoryBeanOne =
                entityManagerFactoryBeanOne(builder);
        return new JpaTransactionManager(factoryBeanOne.getObject());
    }
}

代码解释:

  • 使用@EnableJpaRepositories注解来进行JPA 的配置,该注解中主要配置三个属性:basePackages、entityManagerFactoryRef 以及transactionManagerRef。其中,basePackages用来指定Repository所在的位置, entityManagerFactoryRef用来指定实体类管理工厂Bean的名称,transactionManagerRef则用来指定事务管理器的引用名称,这里的引用名称就是JpaConfigOne类中注册的Bean的名称(默认的Bean 名称为方法名)。
  • 第13~17行创建LocalContainerEntityManagerFactoryBean,该Bean将用来提供EntityManager实例,在该类的创建过程中,首先配置数据源,然后设置JPA 相关配置(JpaProperties 由系统自动加载),再设置实体类所在的位置,最后配置持久化单元名,若项目中只有一个EntityManagerFactory,则persistenceUnit可以省略掉,若有多个,则必须明确指定持久化单元名。
  • 由于项目中会提供两个LocalContainerEntityManagerFactoryBean实例,第11行的注解@Primary表示当存在多个LocalContainerEntityManagerFactoryBean实例时,该实例将被优先使用。
  • 第21~27行表示创建一个事务管理器。JpaTransactionManager提供对单个EntityManagerFactory 的事务支持,专门用于解决JPA中的事务管理。

这是第一个JPA配置,第二个与之类似,代码如下:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.example.demo.dao2", entityManagerFactoryRef = "entityManagerFactoryBeanTwo", transactionManagerRef = "platformTransactionManagerTwo")
public class JpaConfigTwo {
    @Resource(name = "dsTwo")
    DataSource dsTwo;
    @Autowired
    JpaProperties jpaProperties;

    @Bean
    LocalContainerEntityManagerFactoryBean entityManagerFactoryBeanTwo(EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dsTwo)
                .properties(jpaProperties.getProperties())
                .packages("com.example.demo.model")
                .persistenceUnit("pu2")
                .build();
    }

    @Bean
    PlatformTransactionManager platformTransactionManagerTwo(
            EntityManagerFactoryBuilder builder) {
        LocalContainerEntityManagerFactoryBean factoryBeanTwo =
                entityManagerFactoryBeanTwo(builder);
        return new JpaTransactionManager(factoryBeanTwo.getObject());
    }
}

JpaConfigTwo的配置与JpaConfigOne类似,注意LocalContainerEntityManagerFactoryBean实例不需要添加@Primary注解。

7. 创建Repository

根据第3步的配置,分别在org.sang.daol和 org.sang.dao2包下创建两个Repository。

UserDao 如下:

public interface UserDao extends JpaRepository<User,Integer> {
}

UserDao2 如下:

public interface UserDao2 extends JpaRepository<User,Integer> {
}

UserDao和UserDao2将操作不同的数据源。

8. 创建Controller

简便起见,这里省略掉Service层,将UserDao直接注入 Controller 中,代码如下:

@RestController
public class UserController {
    @Autowired
    UserDao userDao;
    @Autowired
    UserDao2 userDao2;

    @GetMapping("/test1")
    public void test1() {
        User u1 = new User();
        u1.setAge(55);
        u1.setName("鲁迅");
        u1.setGender("男");
        userDao.save(u1);
        User u2 = new User();
        u2.setAge(80);
        u2.setName("泰戈尔");
        u2.setGender("男");
        userDao2.save(u2);
    }
}

9. 测试

在浏览器中输入“http:/localhost:8080/test1”,然后查看数据库,即可看到数据库中的表和数据都已经存在了。