本章是《 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步,就能搞定一切:
- 在pom.xml中配置
spring-boot-starter-data-jpa
,及在 application配置文件中配置数据库连接。 - 编写 Entity 类,依照 JPA 规范,定义实体。
- 编写 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"的表:
表示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 的父类也定义了很多方法,如:
而 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
可以看到, 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 文件的内容如下所示:
<?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
方法把数据给查出来了,同时控制台有一行输出:
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及微服务 的微信公众号,感兴趣的同学请扫描下面的二维码关注下吧,关注后就可以收到我们定期分享的技术干货哦!