《Spring Boot 实战派》--08.用ORM操作SQL数据库
8. 用ORM操作SQL数据库
本章首先介绍如何使用。RM (JPA. MyBatis )操作数据库;然后讲解常用的查询方式、自定 义查询方式、原生SQL (Structured Query Language,结构化查询语言)的开发和映射,
还会深 入地讲解一对一、一对多、多对多的关系映射操作以及事务的使用;最后对比分析JPA和MyBatis 的区别
8.1 认识Java的数据库连接模板JDBCTemplate
8.1.1 认识 J DBCTemplate
1. 了解 JDBC
我们先来了解一下 JDBC( Java DataBase Connectivity )。 它是Java用于连接数据库的规范,也就是用于执行数据库SQL语句的Java API。从JDBC的名 称上看,它似乎没有指定某种数据库。可以猜想它可以为多种数据库提供统一访问的接口,这更符 合程序设计的模式。实际上,它由一组用Java语言编写的类和接口组成,为大部分关系型数据库 提供访问接口。
JDBC需要每次进行数据库连接,然后处理SQL语句、传值、关闭数据库。如果都由开发人 员编写代码,则很容易出错,可能会出现使用完成之后,数据库连接忘记关闭的情况。这容易导致 连接被占用而降低性能,为了减少这种可能的错误,减少开发人员的工作量,JDBCtemplate就被 设计出来了。
2, 了解 JDBCTemplate
JDBCTemplate = JDBC+Template的组合,是对JDBC的封装。它更便于程序实现,替我们完成所有的JDBC底层工作。因此,对于数据库的操作,再不需要每次都进行连接、打开' 关闭了。
现在通过JDBCtemplate不需要进行全局修改,就可以轻松地应对开发人员常常要面对的增加、 删除、修改和查询操作。
JDBC和JDBCtemplate就像是仓库管理员,负责从仓库(数据库)中存取物品。而后者不需 要“每次进入都开门,取完关门”,因为有电动门自动控制。
下面通过具体使用JDBCTemplate的实例来理解它。
8.1.2 实例25:用JDBCTemplate实现数据的增加、删除、修改和查询
本实例演示如何通过JDBCTemplate实现数据的增加、删除、修改和查询。
1.配置基础依赖
要使用JDBCTemplate,则需要添加其Starter依赖。因为要操作数据库,所以也需要配置数 据库(以MySQL为例)的连接依赖,见以下代码:
org.springframework.boot
spring-boot-starter-jdbc mysql mysql-connector-java runtime
添加完依赖后,还需要配置数据库的连接信息。这样JDBCTemplate才能正常连接到数据库。 在application.properties配置文件中配置数据库的地址和用户信息,见以下代码:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver //配置IP地址、编码、时区和SSL spring.datasource.url=jdbc:mysql://127.0.0.1/book?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true //用户名 spring.datasource.username=root //密码 spring.datasource.password=root
2、新建实体类
新建一个测试实体类User,实现RowMapper类,重写mapRow方法,以便实体字段和数 据表字段映射(对应)映射是指把Java中设置的实体字段和MySQL数据库的字段对应起来,因 为实体的id可以对应数据库字段的u_id,也可以对应id、name等。
如果不重写,则程序不知道如何对应。具体代码如下:
package com.example.demo.model; @Data public class User implements RowMapper(
private int id; private String username; private String password;
//必须重写mapRow方法 @Override public User mapRow(ResultSet resultSet, int i) throws SQLException ( User user = new User(); user.setld(resultSet.getlnt("id")); user.setUsemame(resultSet.getString("username"));
user.setPassword(resultSet.getString("password")); return user; } )
3、 操作数据
JDBCTemplate提供了以下操作数据的3个方法。
- execute:表示“执行”,用于直接执行SQL语句。
- update:表示“更新”,包括新增、修改、删除操作。
- query:表示查询。
下面使用这3个方法来实现数据的増加、删除、修改和查询功能。
(1)创建数据表。
在使用JDBCTemplate之前,需要在控制器中注入JDBCTemplate,然后就可以通过 “execute”方法执行SQL操作了,见以下代码:
@SpringBootTest @RunWith(SpringRunner.class) public class UserControllerTest {
@Autowired private JdbcTemplate jdbcTemplate;
/* * Description:创建表 */
@Test public void createUserTable() throws Exception { String sql = "CREATE TABLE 'user' (\n" + "'id' int(1O) NOT NULL AUTO_lNCREMENT,\n" + "'username' varchar(IOO) DEFAULT NULL,\n" + "'password' varchar(IOO) DEFAULT NULL,\n" + "PRIMARY KEY ('id')\n" + ")ENGINE二InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;\n" + "\n"; jdbcTemplate.execute(sql); } }
(2)添加数据。
添加数据可以通过“update”方法来执行,见以下代码:
@Test public void saveUserTest() throws Exception { String sql = "INSERT INTO user (USERNAME,PASSWORD) VALUES ('longzhiran','123456')"; int rows = jdbcTemplate.update(sql); System.out.println(rows); }
(3)查询数据。
以下代码是根据name查询单个记录,执行下面sql字符串里的SQL语句(SELECT FROM user WHERE USERNAME = ? ); 这里需要通过“query”方法来执行。
@Test public void getUserByNameO throws Exception ( String name="longzhiran"; String sql = "SELECT * FROM user WHERE USERNAME = ?"; Listlist = jdbcTemplate.query(sql, new User(), new ObjectQ(name}); for (User user: list) { System.out.println(user); } }
运行测试,会在控制台中输出以下结果:
User(id=4, username=longzhiran, password=123456)
(4) 查询所有记录。
查询所有记录和查询单个记录一样,也是执行“query”方法。区别是,SQL语句使用了查询 通配符“*”,见以下代码:
@Test public void list() throws Exception( String sql = "SELECT * FROM userjdbct"; ListuserList = jdbcTemplate.query(sql,new BeanPropertyRowMapper(User.class)); for (User userLists : userList) ( System.out.println(userLists); } }
(5) 修改数据。
要进行数据的修改,可以使用“update”方法来实现,见以下代码:
〃修改用户的密码 @Test public void updateUserPassword() throws Exception! Integer id=1; String passWord="999888"; String sql =”UPDATE userjdbct SET PASSWORD = ? WHERE ID = ?"; int rows = jdbcTemplate.update(sql, password, id); System.out.println(rows); }
(6) 删除数据。
这里删除数据并不用DELETE方法,而是通过“update”方法来执行SQL语句中的“DELETE” 方法。
〃通过用户id删除用户 @Test public void deletellserByldO throws Exception! String sql=”DELETE FROM userjdbct WHERE ID =?"; int rows = jdbcTemplate.update(sql, 1); System.out.println(rows); }
8.1.3 认识 ORM
ORM ( Object Relation Mapping )是对象/关系映射。它提供了概念性的、易于理解的数据模 型,将数据库中的表和内存中的对象建立映射关系。它是随着面向对象的软件开发方法的发展而产 生的,面向对象的幵发方法依然是当前主流的幵发方法。
对象和关系型数据是业务实体的两种表现形式。业务实体在内存中表现为对象,在数据库中表 现为关系型数据。内存中的对象不会被永久保存,只有关系型数据库(或NoSQL数据库,或文件) 中的对象会被永久保存。
对象/关系映射(ORM)系统一般以中间件的形式存在,因为内存中的对象之间存在关联和继 承关系,
而在数据库中,关系型数据无法直接表达多对多的关联和继承关系。对象、数据库通过ORM 映射的关系如图8-1所示。
目前比较常用的ORM是国外非常流行的JPA和国内非常流行的MyBatis
8.2 JPA---Java 持久层 API
8.2.1 认识 Spring Data
Spring Data是Spring的一个子项目,旨在统一和简化各类型数据的持久化存储方式,而不拘 泥于是关系型数据库还是NoSQL数据库。
无论是哪种持久化存储方式,数据访问对象(Data Access Objects, DA。)都会提供对对象 的增加、删除、修改和查询的方法,以及排序和分页方法等。
Spring Data 提供了基于这些层面的统一接口(如:CrudRepository、PagingAndSorting- Repository),以实现持久化的存储。
Spring Data包含多个子模块,主要分为主模块和社区模块:
1、主要模块
- Spring Data Commons:提供共享的基础框架,适合各个子项目使用,支持跨数据库持 久化。
- Spring Data JDBC:提供了对 JDBC 的支持,其中封装了 JDBCTemplate。
- Spring Data JDBC Ext:提供了对JDBC的支持,并扩展了标准的JDBC,支持Oracle RAD、高级队列和高级数据类型。
- Spring Data JPA:简化创建JPA数据访问层和跨存储的持久层功能。
- Spring Data KeyValue:集成了 Redis和Riak,提供多个常用场景下的简单封装,便于构 建 key-value 模块。
- Spring Data LDAP:集成了 Spring Data repository 对 Spring LDAP 的支持。
- Spring Data MongoDB:集成了对数据库 MongoDB 支持。
- Spring Data Redis:集成了对 Redis 的支持。
- Spring Data REST:集成了对 RESTful 资源的支持。
- Spring Data for Apache Cassandra :集成了对大规模、高可用数据源 Apache Cassandra 的支持。
- Spring Data for Apace Geode:集成了对 Apache Geode 的支持。
- Spring Data for Apache Solr:集成了对 Apache Sole 的支持。
- Spring Data for Pivotal GemFire:集成了对 Pivotal GemFire 的支持。
2、社区模块
- Spring Data Aerospike:集成了对 Aerospike 的支持。
- Spring Data ArangoDB:集成了对 ArangoDB 的支持。
- Spring Data Couchbase:集成了对 Couchbase 的支持。
- Spring Data Azure Cosmos DB:集成了对 Azure Cosmos 的支持。
- Spring Data Cloud Datastore:集成了对 Google Datastore 的支持。
- Spring Data Cloud Spanner:集成了对 Google Spanner 的支持。
- Spring Data DynamoDB:集成了对 DynamoDB 的支持。
- Spring Data Elasticsearch:集成了对搜索引擎框架 Elasticsearch 的支持。
- Spring Data Hazelcast:集成了对 Hazelcast 的支持。
- Spring Data Jest:集成了对基于 Jest REST client 的日asticsea「ch 的支持。
- Spring Data Neo4j:集成了对Neo4j数据库的支持。
- Spring Data Vault:集成了对 Vault 的支持。
8.2.2 认识 JPA
JPA (Java Persistence API)是Java的持久化API,用于对象的持久化。它是一个非常强大的ORM持久化的解决方案,免去了使用JDBCTemplate幵发的编写脚本工作。
JPA通过简单 约定好接口方法的规则自动生成相应的JPQL语句,然后映射成POJO对象。
JPA是一个规范化接口,封装了 Hibemate的操作作为默认实现,让用户不通过任何配置即可 完成数据库的操作。JPA、Spring Date和Hibernate的关系如图8-2所示。
Hibernate 主要通过 hibernate-annotationx hibernate-entitymanager hibernate-core 三个组件来操作数据。
- hibernate-annotation:是Hibernate支持annotation方式配置的基础,它包括标准的JPA annotation^ Hibernate 自身特殊功能的 annotationo
- hibernate-core:是Hibemate的核心实现,提供了 Hibernate所有的核心功能。
- hibernate-entitymanager:实现了标准的 JPA,它是 hibernate-core 和 JPA 之间的适 配器,它不直接提供ORM的功能,而是对hibernate-core进行封装,使得Hibernate符 合JPA的规范。
如果要JPA创建8.1.2节中“2.新建实体类”里的实体,可使用以下代码来实现
@Data @Entity public class User { @ld //id的自增由数据库自动管理 @GeneratedValue(strategy = GenerationType. IDENTITY) private int id; private String username; private String password; }
对比JPA与JDBCTemplate创建实体的方式可以看出:JPA的实现方式简单明了,不需要重 写映射(支持自定义映射),只需要设置好属性即可。
id的自增由数据库自动管理,也可以由程序管 理,其他的工作JPA自动处理好了。
8.2.3 使用 JPA
要使用JPA,只要加入它的Starter依赖,然后配置数据库连接信息。
1、添加 JPA 和 MySQL 数据库的依赖
下面以配置JPA和MySQL数据库的依赖为例,具体配置见以下代码:
org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java
runtime
2, 配置数据库连接信息
Spring Boot项目使用MySQL等关系型数据库,需要配置连接信息,可以在 application.properties文件中进行配置。以下代码配置了与MySQL数据库的连接信息:
spring.datasource.url=jdbc:mysql://127.0.0.1/book?useUnicode=true&characterEncoding=utf-8&serverTi mezone=UTC&useSSL=true spring.datasource.usemame=root
spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver
//hibernate的配置属性, 主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置见下表: spring.jpa.properties.hibernate.hbm2ddl.auto=update
// spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5lnnoDBDialect
//开发工具的控制台是否显示SQL语句, 建议打开
spring.jpa.show-sql= true
属性 | 说明 |
create |
每次加载Hibemate时都会删除上一次生成的表,然后根据Model类再重新生成新表,哪怕没有任何 改变也会这样执行,这会导致数据库数据的丢失 |
create-drop |
每次加载Hibernate时会根据Model类生成表,但是sessionFactory 一旦关闭,表就会自动被删除 |
update |
最常用的属性。第一次加载Hibernate时会根据Model类自动建立表的结构(前提是先建立好数据库)。 以后加载Hibernate 会根据Model类自动更新表结构,即使表结构改变了,但表中的数据仍然存在, 不会被删除。要注意的是,当部署到服务器后,表结构是不会被马上建立起来的,要等应用程序第一次运 行起来后才会建立。Update表示如果Entity实体的字段发生了变化,那么直接在数据库中逬行更新 |
validate |
每次加载Hibernate时,会验证数据库的表结构,只会和数据库中的表进行比较,不会创建新表,但 是会插入新值 |
8.2.4 了解JPA注解和属性
1. JPA的常用注解
JPA的常用注解见表
注解 | 说明 |
@ Entity |
声明类为实体 |
@Table |
声明表名,?Entity W@Table注解一般一块使用,如果表名和实体类名相同,那么?Table 可以省略 |
@Basic |
指定非约束明确的各个字段 |
?Embedded |
用于注释属性,表示该属性的类是嵌入类(@embeddable用于注释Java类的,表示类是 嵌入类) |
@ld |
指定的类的属性,一个表中的主键 |
@GeneratedValue |
指定如何标识属性可以被初始化,如@GeneratedValue(strategy=GenerationType. SEQUENCE, generator= urepair_seqn ):表示主键生成策略是sequence,还有 Auto、 Identity、Native 等 |
?Transient |
表示该属性并非一个数据库表的字段的映射,ORM框架将忽略该属性。如果一个属性并非 数据库表的字段映射,就务必将其标示为@T「ansient,即它是不持久的,为虚拟字段 |
?Column |
指定持久属性,即字段名。如果字段名与列名相同,则可以省略。使用方法如: @Column(length=11,name="phone",nullable=false, columnDefinition="varchar(11) unique comment '电话号码 |
@SequenceGenerator |
指定在@GeneratedValue注解中指定的属性的值。它创建一个序列 |
@TableGenerator |
在数据库生成一张表来管理主键生成策略 |
@AccessType |
这种类型的注释用于设置访问类型。如果设置@AccessType (FIELD),则可以直接访问变量,并且不需要使用Getter和Setter方法,但必须为public属性。如果设置@AccessType(PROPERTY),则通过Getter和Setter方法访问Entity的变量 |
@UniqueConstraint |
指定的字段和用于主要或辅助表的唯一约束 |
@ColumnResult |
可以参考使用select子句的SQL查询中的列名 |
@NamedQueries |
指定命名查询的列表 |
@NamedQuery |
指定使用靜态名称的查询 |
@Basic |
指定实体属性的加载方式,如@Basic(fetch=FetchType.LAZY) |
@Jsonlgnore |
作用是JSON序列化时将Java Bean中的一些属性忽略掉,序列化和反序列化都受影响 |
2.映射关系的注解
映射关系的注解见表:
注解 | 说明 |
@JoinColumn |
指定一个实体组织或实体集合。用在“多对一”和“一对多”的关联中 |
@OneToOne |
定义表之间“一对一”的关系 |
@OneToMany |
定义表之间“一对多”的关系 |
@ManyToOne |
定义表之间“多对一”的关系 |
@ManyToMany |
定义表之间“多对多”的关系 |
3.映射关系的属性
映射关系的属性见表
属性名 | 说明 |
targetEntity |
表示默认关联的实体类型,默认为当前标注的实体类 |
cascade |
表示与此实体一对一关联的实体的级联样式类型,以及当对实体进行操作时的策略。 在定义关系时经常会涉及是否定义Cascade (级联处理)属性,如果担心级联处理容易造 成负面影响,则可以不定义。它的类型包括CascadeType.PERSIST (级联新建)、 CascadeType.REMOVE (级联删除)、 CascadeType.REFRESH (级联刷新)、 CascadeType.MERGE(级联更新)、 CascadeType.ALL (级联新建、更新、删除、刷新) |
fetch |
该实体的加载方式,包含LAZY和EAGER |
optional |
表示关联的实体是否能够存在null值。默认为true,表示可以存在null值。如果为false, 则要同时配合使用@JoinColumn标记 |
mappedBy |
双向关联实体时使用,标注在不保存关系的实体中 |
JoinColumn |
关联指定列。该属性值可接收多个@JoinColumn0用于配置连接表中外键列的信息。 @JoinColumn配置的外键列参照当前实体对应表的主键列 |
JoinTable |
两张表通过中间的关联表建立联系时使用,即多对多关系 |
PrimaryKeyJoinColumn |
主键关联。在关联的两个实体中直接使用注解@PrimaryKeyJoinColumn注释。 |
注:
懒加载LAZY和实时加载EAGER的目的是,实现关联数据的选择性加载。
懒加载是在属性被引用时才生成查询语句,抽取相关联数据。
实时加载则是执行完主查询后,不管是否被引用,都会马上执行后续的关联数据查询。
使用懒加载来调用关联数据,必须要保证主查询的Session (数据库连接会话)的生命周期没有结束,否则是无法抽取到数据的。
在 Spring Data J PA 中,要控制 Session 的生命周期,否则会出现 “could not initialize proxy [xxxx#18]-no Session" 错误。
可以在配置文件中配置以下代码来控制Session的生命周期:
spring.jpa.open-in-view=true spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
8.2.5 实例26:用JPA构建实体数据表
下面通过实例来体验如何通过JPA构建对象/关系映射的实体模型。
这里以编写实体Article为例,见以下代码:
package com.example.demo.entity; ?Entity @Data public class Article implements Serializable (
@ld /** * Description: IDENTITY代表由数据库控制,auto代表由Spring Boot应用程序统一控制(有多个表时,id 的自增值不一定从1开始) */ @GeneratedValue(strategy = GenerationType. IDENTITY) private long id; @Column(nullable = false, unique = true) @NotEmpty(message 二"标题不能为空") private String title; /** *Description:枚举类型 7 @Column(columnDefinition=,'enum(,^7 图文、文)”) private String type;//类型 /** *Description: Boolean 类型默认 false */ private Boolean available = Boolean.FALSE; @Size(min=0, max=20) private String keyword; @Size(max = 255) private String description; @Column(nullable = false) private String body; /** * Description:创建虚拟字段 */ ?Transient private List keyword lists; public List getKeywordlists() ( return Arrays.asList(this.keyword.trim().split('T')); } public void setKeywordlists(List keywordlists) (
this, keyword lists = keyword lists; } }
如果想创建虚拟字段,则通过在属性上加注解?Transient来解决。
运行项目后会自动生成数据表
字段 |
类型 |
长度 |
小数点 |
允许空值 |
id |
bigint |
20 |
0 |
否 |
available |
bit |
1 |
0 |
是 |
body |
varchar |
255 |
0 |
否 |
description |
varchar |
255 |
0 |
是 |
keyword |
varchar |
20 |
0 |
是 |
title |
varchar |
255 |
0 |
否 |
type |
enum |
0 |
0 |
是 |
8.3认识JPA的接口
JPA提供了操作数据库的接口。在开发过程中继承和使用这些接口,可简化现有的持久化开发 工作。可以使Spring找到自定义接口,并生成代理类,后续可以把自定义接口注入Spring容器中 进行管理。
在自定义接口过程中,可以不写相关的SQL操作,由代理类自动生成。
8.3.1 JPA 接口 JpaRepository
JpaRepository 继承自 PagingAndSortingRepository。该接口提供了 JPA 的相关实用功能, 以及通过Example进行查询的功能。Example对象是JPA提供用来构造查询条件的对象。该接口 的关键代码如下:
public interface JpaRepositoryextends PagingAndSortingRepository , QueryByExampleExecutor (}
在上述代码中,T表示实体对象,ID表示主键。ID必须实现序列化。
JpaRepository提供的方法见表
方 法 |
描 述 |
List |
查找所有实体 |
List |
排序、查找所有实体 |
List |
返回制定一组ID的实体 |
|
保存集合 |
void flush(); |
执行缓存与数据库同步 |
|
强制执行持久化 |
void deletelnBatch(lterable |
删除一个实体集合 |
void deleteAlllnBatch(); |
删除所有实体 |
T getOne(ID var1); |
返回ID对应的实体。如果不存在,则返回空值 |
|
查询满足Example的所有对象 |
|
查询满足Example的所有对象,并且进行排序返回 |
8.3.2 分页排序接口 PagingAndSortingRepository
PagingAndSortingRepository继承自CrudRepository提供的分页和排序方法。其关键代码 如下:
@NoRepositoryBean public interface PagingAndSortingRepositoryextends CrudRepository {
IterablefindAll(Sort var1); Page findAll(Pageable var1); }
其方法有如下两种:
? IterablefindAII(Sortsort):排序功能。它按照"sort"制定的排序返回数据。 ? Page findAll(Pageable pageable):分页查询(含排序功能)。
8.3.3 数据操作接口 CrudRepository
CrudRepository接口继承自Repository接口,并新增了增加、删除、修改和查询方法。 CrudRepository提供的方法见表
方 法 |
说 明 |
|
保存实体。当实体中包含主键时,JPA会进行更新操作 |
|
保存所有实体。实体必须不为空 |
Optional |
根据主键id检索实体 |
boolean existsByld(ID id) |
根据主键id检索实体,返回是否存在。值为布尔类型 |
lterable |
返回所有实体 |
lterable |
根据给定的一组id值返回一组实体 |
long count() |
返回实体的数量 |
void deleteByld(ID id) |
根据id删除数据 |
void delete(T entity) |
删除给定的实体 |
void deleteAll(lterable<? extends T> entities) |
删除实体 |
void deleteAll() |
删除所有实体 |
8.3.4 分页接口 Pageable 和 Page
Pageable接口用于构造翻页查询,返回Page对象。Page从。开始分页。 例如,可以通过以下代码来构建文章的翻页查询参数。
@RequestMapping(7article") public ModelAndView articleList(@RequestParam(value = "start", defaultvalue = "0") Integer start, @RequestParam(value = "limit", defaultvalue = "10") Integer limit){ start = start < 0 ? 0 : start; Sort sort = new Sort(Sort.Direction.DESC, "id"); Pageable pageable = PageRequest.of(start, limit, sort); Pagepage = articleRepository.findAII(pageable);
ModelAndView mav = new ModelAndView("admin/article/list");
mav.addObject("page", page); return mav; }
然后,再调用它的参数获取总页数、上一页、下一页和末页,见以下代码。
8.3.5 排序类Sort
Sort类专门用来处理排序。最简单的排序就是先传入一个属性列,然后根据属性列的值进行排 序。默认情况下是升序排列。它还可以根据提供的多个字段属性值进行排序。
例如以下代码是通过 Sort.Order对象的List集合来创建Sort对象的:
Listorders = new ArrayList<>(); orders.add(new Sort.Order(Sort.Direction.DESC, "id")); orders.add(new Sort.Order(Sort.Direction.ASC, "view")); Pageable pageable = PageRequest.of(start, limit, sort);
Pageable pageable = PageRequest.of(start, limit, Sort.by(orders));
Sort排序的方法还有下面几种:
- 直接创建Sort对象,适合对单一属性做排序。
- 通过Sort.Order对象创建Sort对象,适合对单一属性做排序。
- 通过属性的List集合创建Sort对象,适合对多个属性采取同一种排序方式的排序。
- 通过Sort.Order对象的List集合创建Sort对象,适合所有情况,比较容易设置排序方式。
- 忽略大小写排序。
- 使用JpaSort.unsafe进行排序。
- 使用聚合函数进行排序。
8.4 JPA的查询方式
8.4.1使用约定方法名
约定方法名一定要根据命名规范来写,Spring Data会根据前缀、中间连接词(Or、And、Like、NotNull等类似SQL中的关键词)、内部拼接SQL代理生成方法的实现。
约定方法名的方法见表:
SQL |
方法例子 |
JPQ L语句 |
and |
findByLastnameAndFirstname |
where x.lastname = ?1 and x.firstname = ?2 |
or |
findByLastnameOrFirstname |
where x.lastname = ?1 or x.firstname = ?2 |
= |
findByFirstname,findByFirstnamels JindByFirstnameEquals |
where x.firstname = ?1 |
between xxx and xxx |
findByStartDateBetween |
where x.startDate between ?1 and ?2 |
< |
findByAgeLessThan |
where x.age < ?1 |
<= |
findByAgeLessThanEqual |
where x.age <= ?1 |
> |
findByAgeGreaterThan |
where x.age > ?1 |
>= |
findByAgeGreaterThanEqual |
where x.age >= ?1 |
> |
findByStartDateAfter |
where x.startDate > ?1 |
< |
findByStartDateBefore |
where x.startDate < ?1 |
is null |
findByAgelsNull |
where x.age is null |
is not null |
findByAge(ls)NotNull |
where x.age not null |
like |
findByFirstnameLike |
where x.firstname like ?1 |
not like |
findByFirstnameNotLike |
where x.firstname not like ?1 |
like 'xxx%' |
findByFirstnameStartingWith |
where x.firstname like ?1 (parameter bound with appended %) |
like 'xxx%' |
findByFirstnameEndingWith |
where x.firstname like ?1 (parameter bound with prepended %) |
like '%xxx%' |
findByFirstnameContaining |
where x.firstname like ?1 (parameter bound wrapped in %) |
order by |
findByAgeOrderByLastnameDesc |
where x.age = ?1 order by x.lastname desc |
<> |
findByLastnameNot |
where x.lastname <> ?1 |
in() |
findByAgeln(Collection ages) |
where x.age in ?1 |
not in() |
findByAgeNotln(Collection ages) |
where x.age not in ?1 |
TRUE |
findByActiveTrue() |
where x.active = true |
FALSE |
findByActiveFalse() |
where x.active 二 false |
接口方法的命名规则也很简单,只要明白And、Or、Is、Equal、Greater、StartingWith等 英文单词的含义,就可以写接口方法。具体用法如下:
public interface UserRepository extends Repository{ List findByEmailOrName(String email, String name); }
上述代码表示,通过email或name来查找User对象。
约定方法名还可以支持以下几种语法:
- User findFirstByOrderByNameAsc()
- Page
queryFirst100ByName(String name, Pageable pageable) - Slice
findTop100ByName(String name, Pageable pageable) - List
findFirst100ByName(String name, Sort sort) - List
findTop100ByName(String name, Pageable pageable)
8.4.2 用JPQL进行查询
JPQL语言(Java Persistence Query Language)是一种和SQL非常类似的中间性和对象 化查询语言,它最终会被编译成针对不同底层数据库的SQL语言,从而屏蔽不同数据库的差异。
JPQL语言通过Query接口封装执行,Query接口封装了执行数据库查询的相关方法。调用 EntityManager的Query、NamedQuery及NativeQuery方法可以获得查询对象,进而可调用 Query接口的相关方法来执行查询操作。
JPQL是面向对象进行查询的语言,可以通过自定义的JPQL完成UPDATE和DELETE操 作。JPQL不支持使用INSERTo对于UPDATE或DELETE操作,必须使用注解@Modifying 进行修饰。
JPQL的用法见以下两段代码。
(1 )下面代码表示根据name值进行查找。
public interface UserRepository extends JpaRepository( @Query("select u from User u where u.name = ?1") User findByName(String name); }
(2)下面代码表示根据name值进行模糊查找。
public interface UserRepository extends JpaRepository{
@Query("select u from User u where u.name like %?1")
ListfindByName(String name);
}
8.4.3用原生SQL进行查询
在使用原生SQL查询时,也使用注解@Query此时,nativeQuery参数需要设置为true。 下面先看一些简单的查询代码。
1、根据ID查询用户
public interface UserRepository extends JpaRepository( 〃根据ID查询用户 @Query(value = "select * from user u where u.id=:id", nativeQuery = true) User findByld (@Param("id")Long id); )
2、查询所有用户
public interface UserRepository extends JpaRepository{ 〃查询所有用户 @Query(value = "select * from user", nativeQuery = true) List findAIINative(); }
3、根据email查询用户
public interface UserRepository extends JpaRepository( 〃根据email查询用户 @Query(value = " select * from user where email= ?1", nativeQuery 二 true) User findByEmail(String email); )
4、根据name查询用户,并返回分页对象Page
public interface UserRepository extends JpaRepository( @Query(value = " select * from user where name= ?1", countQuery = " select count(*) from user where name= ?1", nativeQuery = true) Page findByName(String name, Pageable pageable); }
5、根据名字来修改email的值
@Modifying @Query("update user set email = :email where name =:name")
Void updateUserEmailByName(@Param("name")String name,@Param("email")String email);
6、使用事务
UPDATE或DELETE操作需要使用事务。此时需要先定义Service层,然后在Service层的方法上添加事务操作。
对于自定义的方法,如果需要改变Spring Data提供的事务默认方式,则 可以在方法上使用注解 @Transactional, 如以下代码:
@Service public classUserService {
@Autowired private UserRepository userRepository;
@Transactional public void updateEmailByName(String name,String email)( userRepository.updateUseEmaiByName(name, email); } )
测试代码:
@Test public void testUsingModifingAnnotation(){ userService.updateEmailByName("longzhonghua", "363694485@qq.com"); }
在进行多个Repository操作时,也应该使这些操作在同一个事务中。按照分层架构的思想,这些操作属于业务逻辑层,因此需要在Service层实现对多个Repository的调用,并在相应的方法 上声明事务。
8.4.4 用 Specifications 进行查询
如果要使 Repository 支持 Specification 查询,则需要在 Repository 中继承 JpaSpecificationExecutor接口,具体使用见如下代码:
public interface CardRepository extends JpaRepository, JpaSpecificationExecutor {
Card findByld(long id); }
下面以一个例子来说明Specifications的具体用法:
@SpringBootTest @RunWith(SpringRunner.class) public class testJpaSpecificationExecutor (
@Autowired private CardRepository CardRepository;
@Test public void testJpaSpecificationExecutor() ( int pageNo = 0; int pageSize = 5; PageRequest pageable = PageRequest.of(pageNo, pageSize); //通常使用Specification的匿名内部类 Specificationspecification = new Specification () {
@Override public Predicate toPredicate(Rootroot,CriteriaQuery<?> query, CriteriaBuilder cb) {
Path path = root.get("id"); //gt是大于的意思。这里代表id大于2 Predicate predicatel = cb.gt(path, 2); //equal是等于的意思,代表查询num值为422803的数据记录 Predicate predicate? = cb.equal(root.get("num"), 422803); //构建组合的Predicate Predicate predicate = cb.and(predicate1 ,predicate2);
return predicate; } };
Pagepage = cardRepository.findAII(specification, pageable); System.out.println("S记录数:"+ page.getTotalElements()); System.out.println(”当前第:"+ (page.getNumber() + 1) +"页, System.out.println(',1& 页数:" + page.getTotalPages()); System.out.println("当前页面的 List:" + page.getContent());
System.out.println("当前页面的记录数:"+ page.getNumberOfElements()); } )
代码解释如下:
- CriteriaQuery接口:specific的顶层查询对象,它包含查询的各个部分,比如,select、from、 where、group by、order by等。CriteriaQuery对象只对实体类型或嵌入式类型的Criteria 查询起作用。
- root:代表查询的实体类是Criteria查询的根对象。Criteria查询的根定义了实体类型,能 为将来的导航获得想要的结果。它与SQL查询中的From子句类似。Root实例是类型化的, 且规定了 From子句中能够出现的类型。
查询根实例通过传入一个实体类型给 AbstractQuery.from 方法获得。
- query:可以从中得到Root对象,即告知JPA Criteria查询要查询哪一个实体类。还可 以添加查询条件,并结合EntityManager对象得到最终查询的TypedQuery对象。
- CriteriaBuilder对象:用于创建Criteria相关对象的工厂,可以从中获取到Predicate 对象。
- Predicate类型:代表一个查询条件。
运行上面的测试代码,在控制台会输出如下结果(确保数据库已经存在数据):
Hibernate: select card0_.id as id1_0_, card0_.num as num2_0_ from card cardO_ where card0_.id>2 and card0_.num=422803 limit ? Hibernate: select count(cardO_.id) as col_0_0_ from card card0_ where card0_.id>2 and card0_.num=422803 总记录数:6 当前第:1页 总页数:2 当前页面的 List: [Card(id=4, num=422803), Card(id=8, num=422803), Card(id=10, num=422803), Card(id=20, num=422803), Card(id=23, num=422803)] 当前页面的记录数:5
8.4.5 用 ExampleMatcher 进行查询
Spring Data可以通过Example对象来构造JPQL查询,具体用法见以下代码:
User user?二 new User(); //构建查询条件 user.setName("zhonghua"); ExampleMatcher matcher = ExampleMatcher.matchingO 〃创建一个ExampleMatcher,不区分大小写匹配name .withlgnorePaths("name") 〃包括null值 .withlncludeNullValuesO //执行后缀匹配 .withStringMatcherEndingO; 〃通过Example构建查询 Example< User > example = Example.of(user, matcher); Listlist userRepository.findALL(example);
默认情况下,ExampleMatcher会匹配所有字段。
可以指定单个属性的行为(如name或内嵌属性“name.use"')。如:
- withMatcher("name", endsWith())
- withMatcher("name", startsWith().ignoreCase())
8.4.6用谓语QueryDSL进行查询
QueryDSL也是基于各种ORM之上的一个通用查询框架,它与SpringData JPA是同级别的。 使用QueryDSL的API可以写出SQL语句(Java代码,非真正标准SQL ),不需要懂SQL语句。
它能够构建类型安全的查询。这与JPA使用原生查询时有很大的不同,可以不必再对“Object/ 进行操作。它还可以和JPA联合使用。
8.4.7用NamedQuery进行查询
官方不推荐使用NamedQuery,因为它的代码必须写在实体类上面,这样不够独立。其使用方 法见以下代码:
?Entity @NamedQuery(name = "User.findByName", query = "select u from User u where u.name = ?1")
public class User { )
8.5 实例27:用JPA开发文章管理模块
下面以开发文章管理模块的实例来讲解JPA的用法。
8.5.1实现文章实体
新建Spring Boot项目,然后在项目的业务代码入口下(入口类同级目录下)新建entity、 repositoryx servicex controller文件夹,并在service文件夹中新建impl文件夹。
用鼠标右键单击entity文件夹,在弹岀的下拉菜单中选择“new->java class”命令,在弹出 的窗口中输入“Article”,然后在Article类中输入以下代码:
package com.example.demo.entity; ?Entity @Data public class Article extends BaseEntity implements Serializable {
@ld @GeneratedValue(strategy = GenerationType. IDENTITY)
private long id;
@Column(nullable = false, unique = true) @NotEmpty(message ="标题不能为空") private String title;
@Column(nullable = false) private String body; )
代码解释如下。
? ?Entity:声明它是个实体,然后加入了注解@Data, @Data是Lombok插件提供的注解, 以简化代码,自动生成GetterSetter方法。文章的属性字段一般包含id、title、keyword、
body,以及发布时间、更新时间、处理人。这里只简化设置文章id、关键词、标题和内容。
- @GeneratedValue:将 id 作为主键。GenerationType 为“identity”,代表由数据库统 —控制id的自增。如果属性为“auto",则是Spring Boot控制id的自增。使用identity 的好处是,通过数据库来管理表的主键的自增,不会影响到其他表。
- nullable = false, unique = true:建立唯一索引,避免重复。
- @NotEmpty(message ="标题不能为空'):作为提示和验证消息。
8.5.2实现数据持久层
鼠标右键单击repository文件夹,然后新建接口。在Kind类型处选择接口 interface,将名字 设为ArticleRepository0完成后的代码如下:
package com.example.demo.repository; public interface ArticleRepository extends JpaRepository, JpaSpecificationExecutor { Article findByld(long id); }
这里继承JpaRepository来实现对数据库的接口操作。
8.5.3实现服务接口和服务接口的实现类
通过创建服务接口和服务接口的实现类来完成业务逻辑功能。
(1) 创建服务接口,见以下代码:
package com.example.demo.service; public interface ArticleService (
public ListgetArticleList(); public Article findArticleByld(long id); }
这里定义List (列表)方法,并根据id查找对象的方法。
(1) 编写服务接口的实现。
在impl包下,新建article的impl实现service,并标注这个类为service服务类。
通过implements声明使用ArticleService接口,并重写其方法,见以下代码:
package com.example.demo.service.impl; /* * Description:标注为服务类 */ ?Service public class ArticleServicelmpI implements ArticleService {
@Autowired private ArticleRepository articleRepository;
/** *Description:重写service接口的实现,实现列表功能 */ @Override public ListgetArticleList() { return articleRepository.findAII(); }
/** *Description:重写service接口的实现,实现根据id查询对象功能 */ @Override public Article findArticleByld(long id) { return articleRepository.findByld(id); } }
8.5.4实现增加、删除、修改和查询的控制层API功能
接下来,实现增加、删除、修改和查询的控制层API功能。
package com.example.demo.controller; ?Controller @RequestMapping("article") public class AritcleController {
@Autowired private ArticleRepository articleRepository;
/* * Description:文章列表
*/ @RequestMapping("") public ModelAndView articlelist(@RequestParam(value ="start", defaultvalue = "0") Integer start, @RequestParam(value = "limit", defaultvalue = "5") Integer limit) {
start = start < 0 ? 0 : start; Sort sort = Sort.by(Sort.Direction.DESC, "id"); Pageable pageable = PageRequest.of(start, limit, sort); Pagepage = articleRepository.findAII(pageable); ModelAndView mav = new ModelAndView("article/list"); mav.addObject("page", page); return mav; }
/** * Description:根据id获取文章对象 */ @GetMapping("/(id}") public ModelAndView getArticle(@PathVariable("id") Integer id) (
Article articles = articleRepository.findByld(id); ModelAndView mav = new ModelAndView("article/show"); mav.addObject("article", articles); return mav; }
/** * Description:新增操作视图 */ @GetMapping("/add") public String addArticle() ( return "article/add"; }
/** * Description:新增保存方法 */ @PostMapping("") public String saveArticle(Article model) {
articleRepository.save(model); return "redirect:/article/"; }
/** * Description:删除 */ @DeleteMapping("/{id}") public String del(@PathVariable("id") long id) (
articleRepository.deleteByld(id); return "redirect:"; }
/** * Description:编辑视图 */ @GetMapping("/edit/{id}") public ModelAndView editArticle(@PathVariable("id") long id) {
Article model = articleRepository.findByld(id);
ModelAndView mav = new ModelAndView("article/edit");
mav.addObject("article", model); return mav;
}
}
/** * Description:根据id获取文章对象 */ @GetMapping("/{id}") public ModelAndView getArticle(@PathVariable("id") Integer id) (
Article articles = articleRepository.findByld(id); ModelAndView mav = new ModelAndView("article/show");
mav.addObject("article", articles); return mav; } /** * Description:新增操作视图 */ @GetMapping("/add") public String addArticle() ( return "article/add"; } /** * Description:新增保存方法 */ @PostMapping("") public String saveArticle(Article model) {
articleRepository.save(model); return "redirect:/article/"; } /** * Description:删除 */ @DeleteMapping("/{id}") public String del(@PathVariable("id") long id) (
articleRepository.deleteByld(id); return "redirect:"; } /** * Description:编辑视图 */ @GetMapping("/edit/{id}") public ModelAndView editArticle(@PathVariable("id") long id) {
Article model = articleRepository.findByld(id);
ModelAndView mav = new ModelAndView("article/edit");
mav.addObject("article", model); return mav;
} /** * Description:修改方法 */ @PutMapping("/{id}") public String editArticleSave(Article model, long id) (
model.setld(id); articleRepository.save(model); return "redirect:"; }
代码解释如下。
- @Autowired:即自动装配。Spring会自动将标记为@Autowired的元素装配好。这个注 解可以用到构造器、变量域、方法、注解类型上,该注解用于自动装配。
视图从Bean工厂 中获取一个Bean时, Spring会自动装配该Bean中标记为@Autowired的元素, 而无须手动完成。
- @RequestMapping(“/article") : 用户访问网页的URL。这个注解不限于GET方法。
- ModelAndView:分为Model和View。Model负责从后台处理参数。View就是视图,用 于指定视图渲染的模板。
- ModelAndView mav = new ModelAndView("article/list") : 指定视图和视图路径。
- mav.addObject("page", page):指定传递 page 参数。
8.6 实现自动填充字段
在操作实体类时,通常需要记录创建时间和更新时间。如果每个对象的新增或修改都用手工来 操作,则会显得比较烦琐。这时可以使用Spring Data JPA的注解@EnableJpaAuditing来实现 自动填充字段功能。具体步骤如下。
1、开启JPA的审计功能
通过在入口类中加上注解@EnableJpaAuditing,来开启JPA的Auditing功能。
2、创建基类
创建基类,让其他实体类去继承,见以下代码:
@MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity ( /* * 创建时间 */ @CreatedDate //@DateTimeFormat(pattem 二"yyyy-MM-dd HH:mm:ss") private Long createTime;
/* * 最后修改时间 */ @LastModifiedDate //@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Long updateTime;
/* * 创建人 */ @Column(name = "create_by") @CreatedBy private Long createBy;
/* * 修改人 */ @Column(name = "lastmodified_by") @LastModifiedBy private String lastmodifiedBy;
〃省略 getterx setter 方法 }
3.赋值给 CreatedBy 和 LastModifiedBy
上述代码已经自动实现了创建和更新时间赋值,但是创建人和最后修改人并没有赋值,所以需 要实现“AuditorAware”接口来返回需要插入的值。
这里通过创建一个配置类并重写"getCurrentAuditor”方法来实现,见以下代码:
package com.hua.sb.config; 〃省略 /** * Description:给 Bean 中的 @CreatedBy @LastModifiedBy 注入操作人 */ @Configuration public class InjectAuditor implements AuditorAware{
/** * Description:重写 getCurrentAuditor 方法 */ ?Override public OptionalgetCurrentAuditor() { SecurityContext securitycontext = SecurityContextHolder.getContext(); if (securitycontext == null) ( return null; } if (securityContext.getAuthentication() == null) {
return null; } else { String loginUserName = securityContext.getAuthentication().getName(); Optionalname = Optional.ofNullable(loginUserName); return name; } } }
代码解释如下。
- @Configuration:表示此类是配置类。让Spring来加载该类配置。
- SecurityContextHolder : 用于获取 SecurityContext,其存放了 Authentication 和特定于 请求的安全信息。这里是判断用户是否登录。如果用户登录成功,则获取用户名,然后把用 户名返回给操作人。
4.使用基类
要在其他类中使用基类,通过在其他类中继承即可,用法见以下代码:
public class Article extends BaseEntity { }
8.7掌握关系映射开发
8.7.1认识实体间关系映射
对象关系映射(object relational mapping)是指通过将对象状态映射到数据库列,来开发和 维护对象和关系数据库之间的关系。它能够轻松处理(执行)各种数据库操作,如插入、更新、 删除等。
1、映射方向
ORM的映射方向是表与表的关联(join),可分为两种。
? 单向关系:代表一个实体可以将属性引用到另一个实体。即只能从A表向B表进行联表查询。
? 双向关系:代表每个实体都有一个关系字段(属性)引用了其他实体。
2.、ORM映射类型
- 一对一(@OneToOne):实体的每个实例与另一个实体的单个实例相关联。
- 一对多(@OneToMany): 一个实体的实例可以与另一个实体的多个实例相关联。
- 多对一(@ManyToOne): —个实体的多个实例可以与另一个实体的单个实例相关联。
- 多对多(@ManyToMany): —个实体的多个实例可能与另一个实体的多个实例有关。在 这个映射中,任何一方都可以成为所有者方。
8.7.2 实例28:实现"一对一”映射
“一对一”映射首先要确定实体间的关系,并考虑表结构,还要考虑实体关系的方向性。
若为双向关联,则在保存实体关系的实体中要配合注解@JoinColumn;在没有保存实体关系的实体中,要用mappedBy属性明确所关联的实体。
下面通过实例来介绍如何构建一对一的关系映射。
1.编写实体
(1 )新建Student实体,见以下代码:
?Entity @Data @Table(name = "stdu") public class Student {
@ld @GeneratedValue(strategy = GenerationType.IDENTITY) private long id;
private String name;
@Column(columnDefinition = "enum('male','female')")
private String sex;
@OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "cardjd") //关联的表为 card 表,其主键是 id private Card card; //建立集合,指定关系是"一对一",并且声明它在card类中的名称 }
(2)新建Card实体,见以下代码:
package com.example.demo.entity; ?Entity @Table(name = "card") @Data public class Card {
@ld @GeneratedValue(strategy = GenerationType. IDENTITY) private long id;
private Integer num; )
2. 编写 Repository 层
(1 )编写Student实体的Repository,见以下代码:
package com.example.demo.repository;
public interface StudentRepository extends JpaRepository( Student findByld(long id); Student deleteByld(long id); }
(2)编写Card实体的Repository,见以下代码:
package com.example.demo.repository; public interface CardRepository extends JpaRepository{ Card findByld(long id); )
3. 编写Service层
(1 )编写Student的Service层,见以下代码:
package com.example.demo.service; public interface StudentService (
public ListgetStudentlist(); public Student findStudentByld(long id); }
(2)编写Card的Service层,见以下代码:
package com.example.demo.service;
public interface CardService { public ListgetCardList(); public Card findCardByld(long id); )
4. 编写Service实现
(1 )编写Student实体的Service实现,见以下代码:
package com.example.demo.service.impl; public class StudentServicelmpI implements StudentService {
@Autowired private StudentRepository studentRepository;
?Override public ListgetStudentlist() ( return studentRepository.findAII(); }
?Override public Student findStudentByld(long id) ( return studentRepository.findByld(id); } }
(2)编写CaRd实体的Service实现,见以下代码:
package com.example.demo.service.impl; public class CardServicelmpI implements CardService (
@Autowired private CardRepository cardRepository;
?Override public ListgetCardList() ( return cardRepository.findAII(); )
?Override public Card findCardByld(long id) ( return cardRepository.findByld(id); } }
5. 编写测试
完成了上面的工作后,就可以测试它们的关联关系了,见以下代码:
@SpringBootTest @RunWith(SpringRunner.class) public class oneToOneTest {
@Autowired private StudentRepository StudentRepository;
@Autowired private CardRepository CardRepository;
@Test public void testOneToOne() {
Student studentl = new Student(); studentl .setName("赵大伟, studentl .setSex("male");
Student student2 = new StudentO; student2.setName("Jx 大宝”); student2.setSex("male");
Card cardl = new Card(); cardl .setNum(422802); student1.setCard(card1); studentRepository.save(student1);
studentRepository.save(student2);
Card card2 = new Card(); card2.setNum(422803); cardRepository.save(card2);
/** * Description:获取添加之后的id */ Long id = student1.getld(); /** * Description:删除刚刚添加的studentl */ studentRepository.deleteByld(id); } )
运行代码,在控制台输出如下测试结果:
Hibernate: insert into card (num) values (?) Hibernate: insert into stdu (cardjd, name, sex) values (?, ?, ?) Hibernate: insert into stdu (cardjd, name, sex) values (?, ?, ?) Hibernate: insert into card (num) values (?) Hibernate: select studentO_.id as id1_1_0_, studentO_.card_id as card_id4_1_0_, studentO_.name as name2_1_0_, studentO_.sex as sex3_1_0_, card 1_.id as card1_.num as num2_0_1_from stdu studentO_ left outer join card card1_ on student0_.card_id=card1_.id where studentO_.id=? Hibernate: delete from stdu where id=? Hibernate: delete from card where id=?
可以看到,同时在两个表stdu和card中添加了内容,然后删除了刚添加的有关联的stdu和 card表中的值。如果没有关联,则不删除;
对于双向的“一对一”关系映射,发出端和接收端都要使用注解@OneToOne, 同时 定义一个接收端类型的字段属性和@OneToOne注解中的"mappedBy”属性。
这个在双向 关系的接收端是必需的。在双向关系中,有一方为关系的发出端,另一方是关系的反端,即 “Inverse” 端 (接收端);
8.7.3 实例29:实现“一对多”映射
单向关系的一对多注解@oneToMany,只用于关系的发出端(“一”的一方)。另外,需要关 系的发出端定义一个集合类型的接收端的字段属性。
在一对多关联关系映射中,默认是以中间表方式来映射这种关系的。中间表的名称为“用下画 线连接关系的拥有端(发岀端)和 Inverse端(接收端)”,
中间表两个字段分别为两张表的表名加下画线"_"再加主键组成。
当然,也可以改变这种默认的中间表的映方式。在关系的拥有端,使用@JoinClolum注解定义外键来映射这个关系。
1.编写实体
下面以学校(School)和老师(Teacher)来演示一对多的映射关系。
(1) @OneToMany 中 One 的一方 School, 见以下代码:
package com.example.demo.entity; ?Entity @Data public class School (
@ld @GeneratedValue(strategy = GenerationType.IDENTITY) private long id;
private String name;
// @OneToMany(cascade = CascadeType.ALL) @OneToMany() @JoinColumn(name = "school_id") private ListteacherList; )
(2) @OneToMany 中 Many 的一方--> Teacher,见以下代码:
package com.example.demo.entity; @Data ?Entity public class Teacher {
@ld @GeneratedValue(strategy = GenerationType. IDENTITY) private long id;
private String name;
@ManyToOne private School school; }
2.测试映射关系
Service和Repository层在前面已经讲过,这里并没有区别,所以不再讲解。如果不会,请参考本节提供的源代码。
下面直接测试一对多的关系映射。在测试类中,写入以下代码:
@SpringBootTest @RunWith(SpringRunner.class) public class OneToManyTest {
@Autowired private SchoolRepository schoolRepository; @Autowired private T eacherRepository teacherRepository;
@Test public void add() {
School school 1 = new School(); schooll.setNameC 清华大学”); schoolRepository.save(school1); Teacher teacher = new Teacher(); teacher.setName("long"); teacher.setSchool(school1);
teacherRepository.save(teacher); }
@Test public void find() { School schooll = new School(); school 1 = schoolRepository.findSchoolByld(3); ListteacherList = schooll .getTeacherListQ; System.out.println(school1.getName());
for (Teacher teacher: teacherList) { System.out.println(teacher.getNameO); } }
@Test
public void deleteSchoolByld() {
schoolRepository.deleteByld(5);
}
@Test public void deleteTeacherByld() {
teacherRepository.deleteByld(5);
} }
(1 )运行测试add方法,在控制台输出如下结果:
Hibernate: insert into school (name) values (?) Hibernate: insert into teacher (name, schooljd) values (?, ?)
运行测试find方法,在控制台输出如下结果:
2019-04-28 00:30:30.662 INFO 8484 [ main] o.h.h.i.QueryTranslatorFactorylnitiator: HHH000397: Using ASTQueryTranslatorFactory Hibernate: select school0_.id as id1_0_, school0_.name as name2_0_ from school school0_ where school0_.id=? 清华大学 Hibernate: select teacherlisO_.school_id as school_i3_1_0_, teacherlisO_.id as id1_1_0_, teacherlisO_.id as teacherlis0_.name as name2_1_1_, teacherlisO_.school_id as school_i3_1_1_ from teacher teacherlis0_ where teache「lis0_.school_id =? Hibernate: select school0_.id as id1_0_0_, school0_.name as name2_0_0_ from school school0_ where school0_.id=? long
(2) 运行测试deleteSchoolByld方法,在控制台输出如下结果:
Hibernate: select school0_.id as id1_0_0_, school0_.name as name2_0_0_
from school school0_ where school0_.id=? Hibernate: update teacher set school_id=null where school_id=? Hibernate: delete from school where id=?
可以看到,先将所有Teacher表的外键设置为空,然后删除School表的指定值。
运行测试deleteTeacherByld方法,在控制台输岀如下结果:
Hibernate: select teacherO_.id as id1_1_0_, teacherO_.name as name2_1_0_, teacherO_.school_id as school_i3_1_0_, school 1_.id as id1_0_1_, school1_.name as name2_0_1_ from teacher teacherO_
left outer join school school 1_
on teacher0_.school_id=school1_.id where teacherO_.id=?
Hibernate: delete from teacher where id=?
可见是直接删除指定Teacher表的值,并没有删除School表的数据。
在双向一对多关系中,注解@OneToMany(mappedBy= '发出端实体名称小写')用于关系的发出端(即“One”的一方),同时关系的发出端需要定义一个集合类型的接收端的字段属性;
注解@ManyToOne用于关系的接收端(即“Many”的一方),关系的接收端需要定义一个发出端的字段属性。
8.7.4实例30:实现"多对多”映射
在“多对多”关联关系中,只能通过中间表的方式进行映ft,不能通过增加外键来实现。
注解@ManyToMany用于关系的发出端和接收端。关系的发出端定义一个集合类型的接收端 的字段属性,关系的接收端不需要做任何定义。
1.创建实体
(1 )创建Student实体,见以下代码:
package com.example.demo.entity; @Entity @Data public class Student (
@ld @GeneratedValue(strategy = GenerationType.IDENTITY) private long id;
private String name; @Column(columnDefinition ="enum('male','female')") private String sex;
@ManyToMany(fetch=FetchType.LAZY) JoinTable(name="teacher_student",joinColumns={@JoinColumn(name="s_id")},inverseJoinColumns=(@J oinColumn(name="tJd"))) private Setteachers;
}
(2)创建Teacher实体,见以下代码:
package com.example.demo.entity; @Data ?Entity public class Teacher {
@ld @GeneratedValue(strategy = GenerationType. IDENTITY) private long id;
private String name;
@ManyToMany(fetch=FetchType LAZY) @JoinTable(name="teacher_student,,,joinColumns={@JoinColumn(name="t_id")),inverseJoinColumns=( @JoinColumn(name="s_id")}) private Setstudents; )
在“多对多”关系中需要注意以下几点:
- 关系两方都可以作为主控。
- 在 joinColumns 的@JoinColumn(name="tJd")中,t_id 为 JoinTable 中的外键。由于 Student 和 Teacher 的主键都为 id,所以这里省略了 referencedColumnName="id"0
- 在设计模型之间的级联关系时,要考虑好应该采用何种级联规则。
- 如果设置 cascade 二 CascadeType.PERSIST,则在执行 save 时会调用 onPersist()方 法。这个方法会递归调用外联类(Student或Teacher)的onPersist ()进行级联新增。
但因为值已经添加了,所以会报detached entity passed to persist错误,将级联操作取消 (去掉 “cascade = CascadeType.PERSIST”)即可。
2.创建测试
由于Service和Repository层和8.7.3节中的一样,所以这里不再重复写代码,直接进入测 试层的代码编写。如果读者不清楚怎么实现,请具体查看本节的源代码。创建以下测试代码:
@SpringBootTest @RunWith(SpringRunner.class)
public class ManyToManyTest (
@Autowired private StudentRepository studentRepository;
@Autowired private TeacherRepository teacherRepository;
@Test public void add() { Setteachers = new HashSet<>(); Set students = new HashSeto();
Student studentl = new Student();
studentl.setName("zhonghua");
students.add(student1);
studentRepository.save(studentl);
Student student2 = new Student();
student2.setName("zhiran");
students.add(student2);
studentRepository.save(student2);
Teacher teacherl = new Teacher();
teacher1.setName("龙老师");
teacherl.setStudents(students); teachers.add(teacher1);
teacherRepository.save(teacherl); } }
运行测试类,在控制器中输岀如下结果:
Hibernate: insert into student (name, sex) values (?, ?) Hibernate: insert into student (name, sex) values (?, ?) Hibernate: insert into teacher (name) values (?) Hibernate: insert into teacher_student (t_id, s_id) values (?, ?) Hibernate: insert into teacher_student (t_id, sjd) values (?, ?)
对于双向ManyToMany关系,注解@ManyToMany用于关系的发出端和接收端。另外, 关系的接收端需要设置@ManyToMany(mappedBy=,集合类型发出端实体的字段名称)。
8.8 认识MyBatis一 Java数据持久层框架
MyBatis和JPA -样,也是一款优秀的持久层框架,它支持定制化SQL、存储过程,以及高 级映射。它可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJOs ( Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。
MyBatis 3提供的注解可以取代XML。
例如,使用注解?Select直接编写SQL完成数据查询; 使用高级注解@SelectProvider还可以编写动态SQL,以应对复杂的业务需求。
8.8.1 CRUD 注解
增加、删除、修改和查询是主要的业务操作,必须掌握这些基础注解的使用方法。MyBatis提 供的操作数据的基础注解有以下4个。
- @Select:用于构建查询语句。
- @lnsert:用于构建添加语句。
- @Update:用于构建修改语句。
- @Delete:用于构建删除语句。
下面来看看它们具体如何使用,见以下代码:
@Mapper public interface UserMapper{
@Select("SELECT * FROM user WHERE id = #(id}") User queryByld(@Param("id") int id);
@Select("SELECT * FROM user") ListqueryAll();
@Insert(("INSERT INTO user(name,age) VALUES(#{name),#{age})")) int add(User user);
@Delete("DELETE FROM user WHERE id = #{id}”) int delByld(int id);
@Update("UPDATE user SET name=#{name},age=#(age} WHERE id = #(id}") int updateByld(User user);
@Select(”SELECT * FROM user") PagegetUserList(); )
从上述代码可以看出:首先要用@Mapper注解来标注类,把UserMapper这个DAO交给 Spring管理。这样Spring会自动生成一个实现类,不用再写UserMapper的映射文件了。最后使 用基础的CRUD注解来添加要实现的功能。
8.8.2映射注解
MyBatis的映射注解用于建立实体和关系的映射。它有以下3个注解。
? @Results :用于填写结果集的多个字段的映射关系。
? ?Result :用于填写结果集的单个字段的映射关系。
? @ResultMap : 根据 ID 关联 XML 里面的
可以在查询SQL的基础上,指定返回的结果集的映射关系。其中,property表示实体对象的 属性名,column表示对应的数据库字段名。使用方法见以下代码:
@Results({ @Result(property = "username", column = "USERNAME"), @Result(property = "password", column = "PASSWORD") }) @Select("select * from user") Listlist();
8.8.3高级注解
1、高级注解
MyBatis 3.x版本主要提供了以下4个CRUD的高级注解。
- @SelectProvider:用于构建动态查询SQL。
- @InsertProvider:用于构建动态添加SQL。
- @UpdateProvider:用于构建动态更新SQL。
- @DeleteProvider:用于构建动态删除SQL。
高级注解主要用于编写动态SQL。这里以@SelectProvider为例,它主要包含两个注解属性, 其中,type表示工具类,method表示工具类的某个方法(用于返回具体的SQL )。
以下代码可以构建动态SQL,实现查询功能:
@Mapper public interface UserMapper {
@SelectProvider(type = UserSql.class, method = "listAll") ListlistAllUser(); }
UserSql 工具类的代码如下:
public class UserSql {
public String listAll() { return "select * from user; }
}
2、MyBatis3注解的用法举例
(1 )如果要查询所有的值,则基础CRUD的代码是:
@Select("SELECT * FROM user3") ListqueryAll();
也可以用映射注解来一一映射,见以下代码:
//使用注解编写SQL, 完成映射
@Select("select * from user3") @Results({ @Result(property = "id", column = "id"), @Result(property = ''name", column = "name"), @Result(property = "age", column = "age") }) ListlistAll;
(2)用多个参数进行查询。
如果要用多个参数进行查询,则必须加上注解@Param,否则无法使用EL表达式获取参数。
UserMapper接口的写法如下:
@Select("select * from user where name like #(name} and age like #(age}") User getByNameAndAge(@Param("name") String name, @Param("age") int age);
对应的控制器代码如下:
@RequestMapping("/querybynameandage") User querybynameandage(String name, int age) { return userMapper.getByNameAndAge(name,age); }
还可以根据官方提供的API来编写动态SQL:
public String getUser(@Param("name") String name, @Param("age") int age) {
return new SQL() {{ SELECT("*"); FROMC("user3"); if (name != null && age != 0) { WHERE("name like #(name} and age like #{age}"); } else { WHERE("1=2"); } }).toString();
8.9 实例31:用MyBatis实现数据的增加、删除、修改、查询和分页
本节以实现常用的数据增加、删除、修改、查询和分页功能,来体验和加深对MyBatis的知识 和使用的理解。
8.9.1创建Spring Boot项目并引入依赖
创建一个Spring Boot项目,并引入MyBatis和MySQL依赖,见以下代码:
org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 2.0.0 mysql mysql-connector-java runtime org.projectlombok lombok true
8.9.2实现数据表的自动初始化
(1 )在项目的"resources"目录下新建db目录,并添加“schema.sql”文件,然后在此文 件中写入创建user表的SQL语句,以便进行初始化数据表。具体代码如下:
DROP TABLE IF EXISTS 'user'; CREATE TABLE 'user' ( 'id' int(11) NOT NULL AUTOJNCREMENT, 'name' varchar(255) DEFAULT NULL, 'age' int(11) DEFAULT NULL, PRIMARY KEY ('id') )ENGINE=lnnoDB DEFAULT CHARSET=utf8;
(2)在application.properties配置文件中配置数据库连接,并加上数据表初始化的配置。具 体代码如下:
spring.datasource.initialize=true spring.datasource.initialization-mode=always
spring.datasource.schema=classpath:db/schema.sql
完整的 application.properties 文件如下:
spring.datasource.url=jdbc:mysql://127.0.0.1/book?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true spring.datasource.usemame=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update spring.jpa. properties.hibernate.dialect=org.hibernate.dialect.MySQL5lnnoDBDialect
spring.datasource.initialize=true spring.datasource.initialization-mode=always spring.datasource.schema=classpath:db/schema.sql
这样,Spring Boot在启动时就会自动创建user表。
8.9.3实现实体对象建模
用MyBatis来创建实体,见以下代码:
package com.example.demo.model; import lombok.Data;
@Data public class User { private int id; private String name; private int age; }
从上述代码可以看出,用MyBatis创建实体是不需要添加注解@Entity的, 因为@Entity属于JPA的专属注解。
8.9.4实现实体和数据表的映射关系
实现实体和数据表的映射关系可以在Mapper类上添加注解?Mapper,见以下代码。
建议以 后直接在入口类加@MapperScan("com.example.demo.mapper"),如果对每个 Mapper 都加注 解则很麻烦。
package com.example.demo.mapper; @Mapper public interface UserMapper {
@Select("SELECT * FROM user WHERE id 二 #{id)") User queryByld(@Param("id") int id);
@Select(HSELECT * FROM user") ListqueryAll();
@lnsert(("INSERT INTO user(name,age) VALUES(#(name},#(age})"))
int add(User user);
@Delete(”DELETE FROM user WHERE id = #(id}")
int delByld(int id);
@Update("UPDATE user SET name=#(name),age=#(age} WHERE id 二 #(id)")
int updateByld(User user); }
8.9.5实现增加、删除、修改和查询功能
创建控制器实现操作数据的API,见以下代码:
package com.example.demo.controller; @RestController @RequestMapping("/user") public class UserController (
@Autowired UserMapper userMapper;
@RequestMapping("/queryById") User queryById(int id) { return userMapper.queryByld(id); }
@RequestMapping("") ListqueryAll() { return userMapper.queryAII(); }
@RequestMapping("/add") String add(User user) { return userMapper.add(user) == 1 ? "success": "failed"; }
@RequestMapping("/updatebyid") String updateByld(User user) { return userMapper.updateByld(user) == 1 ? "success": "failed"; }
@RequestMapping("/delById") String delById(int id) ( return userMapper.delByld(id) == 1 ? "success": "failed"; } }
(1 )启动项目,访问 http://localhost:8080/user/add?name=long&age=1,会自动添加一个 name=long、age=1 的数据。
(2) 访问 http://localhost:8080/user/updatebyid?name=zhonghua&age=28&id=1,会实 现对id=1的数据的更新,更新为name=zhonghua、age=28。
(3) 访问 http://localhost:8080/user/querybyid?id=1,可以查找到 id=1 的数据,此时的数 据是 name=zhonghua、age=28。
(4) 访问http://localhost:8080/user/,可以查询出所有的数据。
访问 http://localhost:8080/user/delbyid?id=1,可以删除 id 为 1 的数据
8.9.6配置分页功能
1、增加分页支持
分页功能可以通过PageHelper来实现。要使用PageHelper,则需要添加如下依赖,并增加 Thymeleaf 支持。
com.github.pagehelper pagehelper 4.1.6 org.springframework.boot
spring-boot-starter-thymeleaf
1. 创建分页配置类
创建分页配置类来实现分页的配置,见以下代码:
package com.example.demo.config; @Configuration public class PageHelperConfig (
@Bean public PageHelper pageHelper(){ PageHelper pageHelper = new PageHelper();
Properties p = new Properties(); p.setProperty("offsetAsPageNum", "true"); p.setProperty("rowBoundsWithCount", "true");
p.setProperty('Teasonable", "true");
pageHelper.setProperties(p); return pageHelper; } )
代码解释如下。
? ?Configuration:表示PageHelperConfig这个类是用来做配置的。
? @Bean:表示启动PageHelper拦截器。
? offsetAsPageNum:当设置为 true 时,会将 RowBounds 第 1 个参数 offset 当成 pageNum (页码)使用。
? rowBoundsWithCount : 当设置为true时,使用RowBounds分页会迸行count查询。
? reasonable : 在启用合理化时,如果pageNum<1 ,则会查询第一页;如果 pageNum>pages,则会查询最后一页。
8.9.7实现分页控制器
创建分页列表控制器,用以显示分页页面,见以下代码:
package com.example.demo.controller; ?Controller public class UserListController {
@Autowired UserMapper userMapper;
@RequestMapping("listAll") public String listCategory(Model model, @RequestParam(value="start",defaultValue="0")int start, @RequestParam(value = "size", defaultvalue = "20") int size) throws Exception { PageHelper.startPage(start, size,"id desc"); Listcs = userMapper.queryAll(); Pagelnfo page = new PagelnfoO(cs);
model.addAttribute("page", page); return "list"; } )
代码解释如下。
? start:在参数里接收当前是第几页。
? size:每页显示多少条数据。默认值分别是0和20。
? PageHelper.startPage(start,size,"id desc") : 根据 start、size 进行分页, 并且设置 id 倒排序。
? List
? Pagelnfo
? model.addAttribute("page", page):把 page ( Pagelnfo 对象)传递给视图,以供后续显示。
8.9.8创建分页视图
接下来,创建用于视图显示的list.html,其路径为"resources/template/list.html"
在视图中,通过page.pageNum获取当前页码,通过page.pages获取总页码数,见以下代码:
id name
启动项目,多次访问 "http://localhost:8080/user/add?name=long&age=11"增加数据,然 后访问"http://localhost:8080/listAll"可以查看到分页列表。
但是,上述代码有一个缺陷:显示分页处无论数据多少都会显示“上页、下页”。所以,需要通 过以下代码加入判断,如果没有上页或下页则不显示。
[上页] [下页]
上述代码的作用是:如果是第一页,则不显示“上页”;如果是最后一页,则不显示“下一页”。 还有一种更简单的方法 在Mapper中直接返回page对象,见以下代码:
@Select("SELECT * FROM user") PagegetUserList();
然后在控制器中这样使用:
@RestController public class UserListControllerB {
@Autowired UserMapper userMapper;
@RequestMapping("/listAllb") public PagegetUserList(Integer pageNum, Integer pageSize){
PageHelper.startPage(pageNum, pageSize);
PageuserList= userMapper.getUserList();
return userList; } }
代码解释如下。
? pageNum:页码。
? pageSize:每页显示多少记录。
8.10 比较 JPA 与 MyBatis
JPA基于Hibernate,所以JPA和MyBatis的比较实际上是Hibernate和MyBatis之间 的比较。
1、Hibernate 的优势
- DAO层开发比MyBatis简单,MyBatis需要维护SQL和结果映射。
- 对对象的维护和缓存要比MyBatis好,对增加、删除、修改和查询对象的维护更方便。
- 数据库移植性很好。MyBatis的数据库移植性不好,不同的数据库需要写不同的SQL语句。
- 有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。
2、MyBatis的优势
- 可以进行更为细致的SQL优化,可以减少查询字段(大部分人这么认为,但是实际上 Hibernate-样可以实现)。
- 容易掌握。Hibemate门槛较高(大部分人都这么认为,但是笔者认为关键还是要看编写的 教材是否易读)。
3、简单总结
- MyBatis:小巧、方便、高效、简单、直接' 半自动化。
- Hibernate:强大、方便、高效、复杂、间接、全自动化。
它们各自的缺点都可以依据各自更深入的技术方案来解决。所以,笔者的建议是:
- 如果没有SQL语言的基础,则建议使用JPA。
- 如果有SQL语言基础,则建议使用MyBatis,因为国内使用MyBatis的人比使用JPA的 人多很多。