第三章 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及微服务-公众号二维码

猜你喜欢

转载自my.oschina.net/u/3017144/blog/1503482