第三章 Spring Data JPA

本章是《 Spring Boot 快速入门 》系列教程的第三章,若要查看本系列的全部章节,请点击 这里

目录

  • 简介
  • 源码下载
  • 软件版本
  • JPA简介
  • 在项目中配置JPA
  • 编写实体类
  • 编写 Repository 接口
  • 使用原生SQL查询
  • 总结说明

简介

在上一章《 Spring Boot MVC 》中,我们了解了使用 Spring Boot MVC 来开发 Http Restful API的相关技术,但只处理HTTP请求是不够的,现在的应用程序大多使用了关系型数据库,因此本章我们会带着大家继续 Spring Boot 体验之旅,这次我们将采用 JPA 技术来访问数据库,给 Hello Spring Boot 程序添加带数据库访问演示代码。

源码下载

本章的示例代码放在“码云”上,大家可以免费下载或浏览:

https://git.oschina.net/terran4j/springboot/tree/master/springboot-jpa

软件版本

相关软件使用的版本:

  • Java: 1.8
  • Maven: 3.3.9
  • MYSQL: 5.5

程序在以上版本均调试过,可以正常运行,其它版本仅作参考。

JPA简介

JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体“对象持久化”到数据库中。
JPA技术可以极大的降低了对数据库编程的复杂性,一些简单的增删改查的操作,代码只需要操作对象就可以了,JPA自动的帮你映射成数据库的SQL操作。

不过 JPA 只是标准标准,而 Spring Boot 提供了它的技术实现: Spring Data JPA。不过 Spring Data JPA 也不是重复造轮子,它是基于一个非常著名的ORM框架——Hibernate——之上封装实现的。

Spring Data JPA 极大简化了数据库访问层代码,只要3步,就能搞定一切:

  1. 在pom.xml中配置spring-boot-starter-data-jpa,及在 application配置文件中配置数据库连接。
  2. 编写 Entity 类,依照 JPA 规范,定义实体。
  3. 编写 Repository 接口,依靠 Spring Data 规范,定义数据访问接口(注意,只要接口,不需要任何实现)

另外,如果有复杂的SQL查询,Spring Data JPA 也提供了编写原生 SQL 实现的方式。

在项目中配置JPA

首先,我们要在 pom.xml 文件中添加 spring-boot-starter-data-jpa 的依赖,代码如下:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>terran4j</groupId>
    <artifactId>springboot-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-jpa</name>
    <url>https://git.oschina.net/terran4j/springboot/tree/master/springboot-jpa</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

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

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

        <!-- JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

</project>

注意新增的两个依赖,一个是 spring-boot-starter-data-jpa,它集成了JPA相关的 jar 包;另一个是 mysql-connector-java , 因为本示例中我们要连MYSQL的数据库,所以 mysql jdbc 驱动(java) 是必不可少的。

然后,我们要在application.properties配置文件中配置数据库连接及JPA配置,如下所示:

spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username = root
spring.datasource.password = 

spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql = true

spring.datasource开头的是数据库连接的配置,请注意一定要保持对应的数据库是存在的,并且用户名密码都没错,不然待会程序运行时无法启动。
spring.jpa开发的是 JPA 的配置,spring.jpa.hibernate.ddl-auto 表示每次程序启动时对数据库表的处理策略,有以下可选值:

  • create:
    每次程序启动时,都会删除上一次的生成的表,然后根据你的实体类再重新来生成新表,哪怕两次没有任何改变也要这样执行。
    这种策略适合于执行自动化测试的环境下使用,其它环境请慎用。

  • create-drop :
    每次程序启动时,根据实体类生成表,但是程序正常退出时,表就被删除了。

  • update:
    最常用的属性,第一次程序启动时,根据实体类会自动建立起表的结构(前提是先建立好数据库),以后程序启动时会根据实体类自动更新表结构,即使表结构改变了,但表中的记录仍然存在,不会删除以前的记录。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等 第一次访问JPA时后才会建立。

  • validate :
    每次程序启动时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

因此,建议大多数场景下用 update 就可以了,线上环境(或需要慎重的环境)中用 validate 会更保险一些,没有特殊情况下不建议用 create 及 create-drop 模式。

配置完成后,我们运行下 main 程序(代码如下):

@SpringBootApplication
public class HelloJPAApp {

    public static void main(String[] args) {
        SpringApplication.run(HelloJPAApp.class, args);
    }

}

结果控制台输入里多了一些东西:

......
2017-08-04 15:51:27.017  INFO 20248 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.0.12.Final}
2017-08-04 15:51:27.018  INFO 20248 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2017-08-04 15:51:27.020  INFO 20248 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2017-08-04 15:51:27.086  INFO 20248 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.1.Final}
2017-08-04 15:51:27.666  INFO 20248 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
2017-08-04 15:51:28.230  INFO 20248 --- [           main] org.hibernate.tool.hbm2ddl.SchemaUpdate  : HHH000228: Running hbm2ddl schema update
2017-08-04 15:51:28.424  INFO 20248 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
......

如果控制台输出中没有报错,并且有这之类的输出,表示配置成功了。

编写实体类

要操作数据库数据,首先得建表。然而 JPA 使用起来非常简单,简单得你只需要在Java的实体类上加上一些注解,就可以自动映射成数据库表。

下面是一个实体类的代码:

package com.terran4j.springboot.jpa;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity(name = "t_user") // 定义数据库表名称。
@Table(indexes = { // 定义数据库索引。

        // 唯一索引。
        @Index(name = "ux_user_login_name", columnList = "loginName", unique = true), //

        // 非唯一索引。
        @Index(name = "idx_user_age", columnList = "age"), //
})
public class User {

    /**
     * id, 自增主键。
     */
    @Id
    @GeneratedValue
    @Column(length = 20)
    private Long id;

    /**
     * 用户的登录名。
     */
    @Column(length = 100, nullable = false)
    private String loginName;

    /**
     * 用户的年龄。
     */
    @Column(length = 3)
    private Integer age;

    /**
     * 用户的状态。
     */
    @Column(length = 16, nullable = false)
    @Enumerated(EnumType.STRING)
    private UserStatus status = UserStatus.enable;

    /**
     * 用户的注册时间。
     */
    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false)
    private Date registTime;

    public final Long getId() {
        return id;
    }

    public final void setId(Long id) {
        this.id = id;
    }

    public final String getLoginName() {
        return loginName;
    }

    public final void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    public final Integer getAge() {
        return age;
    }

    public final void setAge(Integer age) {
        this.age = age;
    }

    public final UserStatus getStatus() {
        return status;
    }

    public final void setStatus(UserStatus status) {
        this.status = status;
    }

    public final Date getRegistTime() {
        return registTime;
    }

    public final void setRegistTime(Date registTime) {
        this.registTime = registTime;
    }
    
}

首先,我们看 User 类上两个注解 @Entity 和 @Table :
@Entity(name = "t_user") 注解 加在 User 上,表示它是一个实体类, 表名是 t_user 。

@Table(indexes = { // 定义数据库索引。

        // 唯一索引。
        @Index(name = "ux_user_login_name", columnList = "loginName", unique = true), //

        // 非唯一索引。
        @Index(name = "idx_user_age", columnList = "age"), //
})

@Table 里面定义了这个表的索引,一个 @Index 注解定义了一个索引, name 属性表示数据库表中索引的名称, columnList 表示对应的 java 属性名称, unique = true 表示此索引是唯一索引。
比如上面的 @Index(name = "ux_user_login_name", columnList = "loginName", unique = true) 表示对 loginName 属性所对应的字段(映射到数据库表中应该是 login_name 字段)建立唯一索引,索引名为ux_user_login_name。
columnList 中可以放多个java属性,中间用逗号隔开,表示联合索引,如:@Index(name = "idx_user_age_name", columnList = "age,loginName") 表示建立 age 与 login_name 字段的联合索引。

注意: java 属性名都是驼峰命名法(如 loginName),而数据库表字段都是下划线命名法(如 login_name),JPA会自动根据java属性名的驼峰命名法映射成数据库表字段的下划线命名法,如 loginName 属性映射到数据库就是 login_name 字段。

这个 User 实体类写好后,我们再运行下之前的 main 程序,然后惊奇的发现:数据库里自动建了一个名为 "t_user"的表:

t_user.png

表示JPA在启动时根据实体类,自动在数据库中创建了对应的表了。

注意: 实体类 User 一定要放在 HelloJPAApp 类所在包中或子包中,不然
HelloJPAApp 启动时 Spring Boot 可能扫描不到。

编写 Repository 接口

有了表之后,我们要写对表进行增删改查的代码,用JPA干这事简直是简单到姥姥家了,只需要继续一个接口就搞定了,请看代码:

package com.terran4j.springboot.jpa;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

}

这样就写完了基本的增删改查的代码? 的确是这样,因为 JpaRepository 接口已经定义了很多方法,JpaRepository 的父类也定义了很多方法,如:

JPA.png

而 Spring Boot JPA又帮你实现了这些方法,你只需要在继承 JpaRepository 时指定了实体类的类对象和 ID 属性的类对象就可以了,如 public interface UserRepository extends JpaRepository<User, Long> 表示实体类是 User, User 中 ID 属性是 Long 类型的。
并且, Spring Boot JPA 扫描到 UserRepository 是 Repository 的子类后,会以动态代理的方式对 UserRepository 进行实现,并将实现的对象作为 Bean 注入到 Spring 容器中,而我们只需要像使用普通的 Spring Bean 一样,用 @Autowired 引入即可使用。

下面,我们编写一个 Controller 类来调用 UserRepository ,如下所示:

package com.terran4j.springboot.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    @Autowired
    private UserRepository userRepository;

    // URL示例: http://localhost:8080/hello/1
    @RequestMapping(value = "/hello/{id}", method = RequestMethod.GET)
    public String sayHello(@PathVariable("id") Long id) {

        User user = userRepository.findOne(id);

        if (user == null) {
            return String.format("User NOT Found: %d", id);
        }

        String name = user.getLoginName();
        return helloService.hello(name);
    }

}

相关的 HelloService 的代码为:

package com.terran4j.springboot.jpa;

import org.springframework.stereotype.Component;

@Component
public class HelloService {

    public String hello(String name) {
        return "Hello, " + name + "!";
    }
    
}

代码中, User user = userRepository.findOne(id); 表示根据 id 从表中查出一条记录,并映射成 User 对象。

为了测试效果,我们先执行以下SQL在数据库中制造点数据:

delete from `t_user`;
insert into `t_user` (`id`, `login_name`, `age`, `regist_time`, `status`) values 
('1','Jim','12','2017-07-26 09:29:47','enable'),
('2','Mike','23','2017-07-25 09:30:54','disable');

然后启动程序,在浏览器中用以下URL访问:

http://localhost:8080/hello/1
sayHello运行效果

可以看到, userRepository.findOne(id) 的确把数据给查出来了。

使用原生SQL查询

然而,JpaRepository 提供的仅是简单的增删查改方法,那遇到复杂的查询怎么办?
Spring Boot JAP 底层是 Hibernate 实现的, Hibernate 提供了 hql 的类SQL语法来编写复杂查询,不过我个人不建议用 HQL,因为毕竟 HQL 与SQL还是有较大不同的,需要学习成本(但这个成本其实是没必要投入的),另外就是一些场景下需要用数据库的特定优化机制时,HQL 实现不了。
所以,我的建议是使用原生 SQL 的方式实现,而 JPA 是提供了这个能力的,下面我介绍一种用在 orm.xml 中写原生 SQL的方法。

假如需求是这样的,我们要查询某一年龄段的 User(如 10岁 ~ 20岁的),SQL大概要这样写:

SELECT * FROM t_user AS u 
WHERE u.age >= '10' AND u.age <= '20' 
ORDER BY u.regist_time DESC 

Spring Boot JAP 约定是把 SQL 写在类路径的 META-INF/orm.xml 文件中(如果 META-INF 文件夹没有就创建一个),文件路径如下:

orm.xml

orm.xml 文件的内容如下所示:

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_0.xsd"
    version="2.1">

    <named-native-query name="User.findByAgeRange" 
            result-class="com.terran4j.springboot.jpa.User">
        <query><![CDATA[
            select * from t_user as u
            where u.age >= :minAge and u.age <= :maxAge
            order by u.regist_time desc
        ]]></query>
    </named-native-query>

</entity-mappings>

<named-native-query>表示是一个“原生SQL查询”, name="User.findByAgeRange"表示给这个查询起了一个名字叫“User.findByAgeRange”,后面代码中会根据这个名字来引用这个查询,result-class="com.terran4j.springboot.jpa.User" 表示SQL查询返回的结果集,每条记录转化成 User 对象。
<query>里面是原生的SQL语句,其中 : 开始的是变量,如上面的SQL,有两个变量 :minAge 和 :maxAge ,这些变量的值,会从外面传入进来。

然后我们可以在 UserRepository 中添加一个findByAgeRange方法来使用这个原生SQL查询,如下代码所示:

package com.terran4j.springboot.jpa;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserRepository extends JpaRepository<User, Long> {

    /**
     * 查询某年龄段范围之内的用户。
     * 
     * @param minAge
     *            最小年龄。
     * @param maxAge
     *            最大年龄。
     * @return
     */
    @Query(nativeQuery = true, name = "User.findByAgeRange")
    List<User> findByAgeRange(@Param("minAge") int minAge, @Param("maxAge") int maxAge);

}

这个findByAgeRange方法上面有一个@Query(nativeQuery = true, name = "User.findByAgeRange")注解,表示这个方法的实现使用名为User.findByAgeRange的查询,此查询是用原生SQL写的;方法参数上有@Param注解,表示将方法的参数值映射到查询中的变量。

最后,我们写一个 Controller 调用这个方法试,如下代码所示:

package com.terran4j.springboot.jpa;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController2 {

    @Autowired
    private UserRepository userRepository;

    // URL示例: http://localhost:8080/users
    @RequestMapping(value = "/users", method = RequestMethod.GET)
    @ResponseBody
    public List<User> findByAgeRange(
            @RequestParam(value = "minAge", defaultValue = "1") int minAge,
            @RequestParam(value = "maxAge", defaultValue = "999") int maxAge) {

        List<User> users = userRepository.findByAgeRange(minAge, maxAge);

        return users;
    }

}

然后访问 URL: http://localhost:8080/users,运行效果如下:

findByAgeRange查询结果

我们看到findByAgeRange方法把数据给查出来了,同时控制台有一行输出:

Hibernate: select * from t_user as u where u.age >= ? and u.age <= ? order by u.regist_time desc

这也是 JPA 底层实际执行的 SQL,也就是把我们写的 SQL 中 :minAge 和 :maxAge 两个变量换成“绑定变量”的方式。

总结说明

本文我们讲解了用 Spring Boot JPA 来访问数据库,是不是觉得用 Spring Boot 开发超级爽呢,本系列这三章就讲到这了,主要是带大家对 Spring Boot 快速上手,后面笔者会努力出更多关于 Spring Boot && Spring Cloud 的技术文章,敬请期待。

点击 这里 可以查看本系列的全部章节。
(本系列的目标是帮助有 Java 开发经验的程序员们快速掌握使用 Spring Boot 开发的基本技巧,感受到 Spring Boot 的极简开发风格及超爽编程体验。)

另外,我们有一个名为 SpringBoot及微服务 的微信公众号,感兴趣的同学请扫描下面的二维码关注下吧,关注后就可以收到我们定期分享的技术干货哦!

SpringBoot及微服务-公众号二维码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 简介: 本文由浅入深地讲述了使用 Spring Data JPA 需要关注的各个方面,为读者了解和使用该框架提供了...
    AiPuff阅读 4,489评论 1 28
  • 一直都想写点东西,但是又不知道自己写点什么,犹犹豫豫了那么久,终于还是动了想法。 我一直都认为我是一个独...
    老贼别跑阅读 1,190评论 0 0
  • 熟悉的场景 2017年伊始,一个午饭桌上,我的一位同事信誓旦旦的说:“今年我的目标是学习英语;出去旅游2次;找到一...
    凯凯刘阅读 376评论 0 1