Spring Data JPA的简单入门

前言

spring data JPA是spring团队打造的sping生态全家桶的一部分,本身内核使用的是hibernate核心源码,用来作为了解java持久层框架基本构成的样本是再好不过的选择。最近闲来无事,构建了一个demo工程,用来阅读spring data JPA源码,这对于宏观了解持久层框架的基本工作、微观分析spring data JPA的原理和优缺点、避免使用过程中采坑,将会有一定的帮助。

基本工程搭建

spring data JPA的使用需要依托于web框架,最简单快速的方式就是使用https://start.spring.io/构建一个包含spring data JPA的spring boot项目,只需要在引导的对应SQL的菜单中选中Spring Data Jpa和响应数据库的Driver即可,目前无论是idae还是eclipse都支持采用这种方式。上述方法的实质还是在最终生成的spring boot项目的pom.xml中加入了相关的依赖,所以也可以直接生成由maven管理的spring boot项目,然后pom.xml中:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>jpademo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jpademo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>


        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/apt</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

其中,spring-boot-starter-web是spring boot框架依赖,spring-boot-starter-data-jpa是Spring Data Jpa依赖,我使用的Mysql数据库,所以引入了mysql-connector-java。 querydsl-jpa和querydsl-apt是在JPA基础上使用querydsl的依赖,可以不引入。最后lombok广泛使用的懒癌工具包。
接下来当然是构建一个数据源,本地需要安装mysql,这部分我就不细说了。数据库安装完成后,创建一个test库,建song表,建表语句:

CREATE TABLE `song` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` char(10) DEFAULT NULL,
  `year` int(11) DEFAULT NULL,
  `length` int(11) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
)
接着项目中application.properties中设置数据库配置:
#driver配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456
#jpa配置
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

spring.jpa.hibernate.ddl-auto是最有意思的一个配置,一不小心可能造成删库的悲惨结果。

  • create:每次运行该程序,没有表格会新建表格,表内有数据会清空。
  • create-drop:每次程序结束的时候会清空表。
  • update:每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
  • validate:运行程序会校验数据与数据库的字段类型是否相同,不同会报错

这里的是否有表格是以应用中配置的Entity实体为依据的,这意味着应用能够根据自身的Entity实体来影响表的创建和表的结构。千言万语汇成一句话,千万不要配置为create。
最后需要在应用中配置相应的实体Entity和实体的Repository。项目目录整体文件结构如下:
[图片上传失败...(image-4639bb-1597031907127)]
其中:

  • JpademoApplication.java:spring boot应用入口
  • TestController.java: 测试用controller接口
  • Song.java: 对应song表的实体类Entity
  • SongRepository: 对应实体类Song的Repository接口

Song实体定义:

@Entity  //定义为实体类
@Table(name = "song")  //映射表配置
@DynamicInsert         //支持动态插入
@DynamicUpdate         //支持动态更新
//以下为lombok懒癌注解
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Song {
    @Id              //主键id
    @GeneratedValue  //主键自动生成
    private Long id;
    private String name;
    private Integer year;
    private Integer length;
    private String type;
}

Jpa实体Entity对应了持久层的表数据,Song实体的定义,就对应了本地mysql数据test库中的song表。而对表数据的增删改查操作,可以通过Repository实现。

public interface SongRepository extends JpaRepository<Song,Integer>{
    Song findById(Long id);

    Song getById(Long id);

    Song queryByIdAndYear(Long id, Long year);

    void deleteById(Long id);
}

SongRepository继承JpaRepository接口,默认实现了诸如save(S entity)、findAll()、count()、delete(T entity)之类的方法,同时也支持子类通过名字拓展自定义新的查询或其他操作数据的方式,比如我在上面定义的find、get、query By id或者 IdAndYear,就是通过id或者year组合查询。虽然这种通过名字来拓展新的操作数据接口的方法看起来比较蠢,但是具体到实现原理还是值得探讨一番的。

基本使用

了解一个事物基本原理,首先需要全面的了解事物的外在功能。对于spirng data jpa,主要的数据操作方式有使用Repository和使用EntityManager这两种形式。
@Slf4j
@RestController
public class TestController {

    @Resource
    private EntityManager entityManager;

    @Resource
    private SongRepository songRepository;


    @GetMapping("/")
    public List<Song> test() {
        return repositoryList();
    }


    public List<Song> repositoryList() {
        //Repository方式
        List<Song> songs = songRepository.findAll();
        return songs;
    }

    @Transactional
    public void repositorySave() {
        Song song = new Song();
        song.setName("MyHeart");
        song.setLength(230);
        song.setYear(2020);
        songRepository.save(song);
    }

    @Transactional
    public void entityManagerMerge() {
        //EntityManager方式
        Song song = new Song();
        song = new Song();
        song.setName("Journey");
        song.setLength(230);
        song.setYear(2020);
        entityManager.merge(song);
    }

    public List<Song> hqlList() {
        //hql方式
        Query query = entityManager.createQuery("select s as ss from Song s");
        List<Song> songs = query.getResultList();
        return  songs;
    }

    public List<Song> sqlList() {
        //sql方式
        Query nativeQuery = entityManager.createNativeQuery("select * from song s where name LIKE 'MyHeart'");
        List<Song> songs = nativeQuery.getResultList();
        return songs;
    }
}

Repository提供标准Crud接口,也支持按照规范的命名标准自定义接口,这些在上文已经介绍过了。EntityManager方式其实是Repository的底层实现,这种方式提供了创建CriteriaQuery(上例没有展示)、hql 的Query、SQL的Query,通过Query完成最终和数据库的交互。从上例可以看出hql形式和sql比较类似,只是hql在“select s as ss from Song s”语句中,使用了面向对象的封装,这里Song对应的是实体Song,而不表名。原生NativeQuery对应的就是原生Sql查询。
除此之外,Repository也提供非规范命名的接口查询,本质也是使用EntityManager的hql和sql方式。在Repository的接口方法上可以使用@Query注解,并定义相应的hql或者sql的语句,查询条件需要和接口方法的入参一致,查询结果对应接口方法的返回参数。
实际上,spring data jpa提供了一个sql演化的基本思路,即从sql到hql再到结合实体使用的Repository。但是实体很难解决关系型数据库表数据的join问题,在《java数据库持久层框架基础:为什么不是JPA?》这篇文章中,我提到了可以使用querydsl解决联表问题,实际上spring data jpa也有自己的方案,即在单个实体中定义映射集合。简单解释来说,假设一个表对应实体类Person,Person包含一个集合属性Head,Head对应的是另一个表中的数据。Person实体中使用

 @JoinTable(name = "head", joinColumns = { @JoinColumn(nullable = false, name = "item_id", referencedColumnName = "id") })

实现联表。但这种方式存在复杂的级联关系,在更新、插入和删除操作中极其难以处理。

作者:孙新【滴滴出行软件开发工程师】