《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 = ?";
  List list = 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";
  List userList = 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 JpaRepository extends PagingAndSortingRepository, QueryByExampleExecutor (}

在上述代码中,T表示实体对象,ID表示主键。ID必须实现序列化。

JpaRepository提供的方法见表

方 法

描 述

List findAll();

查找所有实体

List findAll(Sort var1);

排序、查找所有实体

List findAIIByld(lterable var1);

返回制定一组ID的实体

List saveAll(lterable var1);

保存集合

void flush();

执行缓存与数据库同步

S saveAndFlush(S var1);

强制执行持久化

void deletelnBatch(lterable var1);

删除一个实体集合

void deleteAlllnBatch();

删除所有实体

T getOne(ID var1);

返回ID对应的实体。如果不存在,则返回空值

List findAll(Example var1);

查询满足Example的所有对象

List findAll(Example var1, Sort var2);

查询满足Example的所有对象,并且进行排序返回

8.3.2 分页排序接口 PagingAndSortingRepository

PagingAndSortingRepository继承自CrudRepository提供的分页和排序方法。其关键代码 如下:

@NoRepositoryBean
public interface PagingAndSortingRepository extends CrudRepository { 
  Iterable findAll(Sort var1);   Page findAll(Pageable var1); }

 

其方法有如下两种:

? IterablefindAII(Sortsort):排序功能。它按照"sort"制定的排序返回数据。
? Page findAll(Pageable pageable):分页查询(含排序功能)。

8.3.3 数据操作接口 CrudRepository

CrudRepository接口继承自Repository接口,并新增了增加、删除、修改和查询方法。 CrudRepository提供的方法见表

 

 

方 法

说 明

S save(S entity)

保存实体。当实体中包含主键时,JPA会进行更新操作

lterable saveAll(lterable entities)

保存所有实体。实体必须不为空

Optional findByld(ID id)

根据主键id检索实体

boolean existsByld(ID id)

根据主键id检索实体,返回是否存在。值为布尔类型

lterable findAll()

返回所有实体

lterable findAIIByld(lterable ids)

根据给定的一组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);
  Page
page = 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对象的:

  

List orders = 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")
   List findByName(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的匿名内部类     Specification specification = new Specification() {
        @Override         public Predicate toPredicate(Root root,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;         }     };
    Page page = 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);
List list 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 List
getArticleList();   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 List
getArticleList() {     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);     Page
page = 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 Optional getCurrentAuditor() {     SecurityContext securitycontext = SecurityContextHolder.getContext();     if (securitycontext == null) (       return null;     }     if (securityContext.getAuthentication() == null) {
      return null;     } else {       String loginUserName = securityContext.getAuthentication().getName();       Optional name = 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 List getStudentlist();   public Student findStudentByld(long id); }

(2)编写Card的Service层,见以下代码:

package com.example.demo.service;
public interface CardService {   public List getCardList();   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 List getStudentlist() (     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 List getCardList() (     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 List teacherList; )

(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);     List teacherList = 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 Set teachers;
}

(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 Set students; )

在“多对多”关系中需要注意以下几点:

  • 关系两方都可以作为主控。
  • 在 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() {     Set teachers = 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")   List queryAll();
  @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")   Page getUserList(); )

  从上述代码可以看出:首先要用@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")
List list();

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")   List listAllUser(); }
UserSql 工具类的代码如下:
public class UserSql {
  public String listAll() {     return "select * from user;   }
}

2、MyBatis3注解的用法举例

(1 )如果要查询所有的值,则基础CRUD的代码是:

@Select("SELECT * FROM user3")
List queryAll();

也可以用映射注解来一一映射,见以下代码:

//使用注解编写SQL, 完成映射
@Select("select * from user3") @Results({     @Result(property = "id", column = "id"),   @Result(property = ''name", column = "name"),   @Result(property = "age", column = "age") }) List listAll;

(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")   List queryAll();
  @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("")   List queryAll() {     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");     List cs = 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:根据返回的集合创建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") Page getUserList();

然后在控制器中这样使用:

@RestController
public class UserListControllerB {
  @Autowired   UserMapper userMapper;
  @RequestMapping("/listAllb")   public Page getUserList(Integer pageNum, Integer pageSize){
    PageHelper.startPage(pageNum, pageSize);
    Page userList= 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的 人多很多。