简洁持久层开发之Spring Data JPA

1前言

Spring项目开发中,持久层的框架常用的有MyBatis、Mybatis Plus、Hibernate和Spring Data JPA等,国内常用的是前两种,本人最先接触的也是MyBatis。下面我列举了常用的三类框架的一些对比信息。今天我们要聊一聊一种解放双手的持久成框架Spring Data JPA。

Mybatis:https://mybatis.org/mybatis-3/zh/java-api.html

Mybatis Plus:https://github.com/baomidou/mybatis Plus

Spring Data JPA:https://spring.io/projects/spring-data-jpa

Mybatis

Mybatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。Mybatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。Mybatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通的 Java 对象)为数据库中的记录。

Mybatis Plus

MybatisPlus 是一款 Mybatis 的增强工具,在 Mybatis 的基础上只做增强,不做改变,为简化开发、提高效率而生。Mybatis Plus 为 Mybatis 提供了一些额外的功能,如:通用 Mapper、通用 Service、分页插件、性能分析插件等。这些功能简化了 CRUD 操作,减少了重复代码的编写,提高了开发效率。

Spring Data JPA

Spring Data JPA 是一个基于 Spring Data 和 JPA(Java Persistence API,Java 持久化 API)技术的持久层框架,它使得开发者可以更加轻松地实现对数据的访问和操作。Spring Data JPA 提供了一套简化 JPA 规范的抽象接口,通过扩展这些接口,可以实现对数据的 CRUD(增删查改)操作。此外,Spring Data JPA 还支持自定义查询、分页查询、排序等功能。

共同点

  1. 持久层框架:Mybatis、Mybatis Plus 和 Spring Data JPA 都是为了简化和优化数据库访问操作的持久层框架。
  2. 减少代码量:三者都旨在减少开发者编写 JDBC 代码的工作量,简化数据库访问操作。
  3. 易于整合:三个框架都可以轻松地与 Spring 框架进行整合,实现依赖注入和事务管理等功能。
  4. 支持 POJO:它们都支持将数据库的表映射为 Java 对象,使得开发者可以更直观地处理数据。
  5. 抽象接口:Mybatis、Mybatis Plus 和 Spring Data JPA 都提供了抽象接口,以简化对数据的 CRUD 操作。

不同点

  1. 定位和发展历史:
    • MyBatis是一个持久层框架,旨在提供对关系型数据库的直接访问,通过XML或注解配置SQL语句和映射。
    • MyBatis Plus是MyBatis的增强版本,提供了更多的便利功能和扩展,如代码生成器、通用Mapper、分页插件等。
    • Spring Data JPA是基于JPA(Java Persistence API)规范的持久层框架,与关系型数据库和ORM(对象关系映射)框架无关,它提供了一种更高级的抽象和简化的方式来操作数据库。
  2. 编程模型:
    • MyBatis和MyBatis Plus使用XML或注解配置SQL语句和映射关系,提供了灵活的SQL编写方式,对SQL的精细控制力度较高。
    • Spring Data JPA使用继承和命名约定来自动生成实体类与数据库表之间的映射,并支持通过方法命名规则自动生成常用的增删改查操作。
  3. 对象关系映射(ORM)支持:
    • MyBatis和MyBatis Plus提供了基本的ORM功能,但需要手动编写SQL语句和映射关系。
    • Spring Data JPA是一个全功能的ORM框架,通过实体类和注解自动完成SQL查询和结果映射,减少了开发人员编写和维护SQL的工作量。
  4. 领域模型支持:
    • MyBatis和MyBatis Plus更加偏向于面向SQL和数据操作的编程模型,不涉及复杂的领域模型定义。
    • Spring Data JPA更加偏向于领域模型的设计,支持实体之间的关系映射、继承关系等高级语义。
  5. 社区和生态系统:
    • MyBatis和MyBatis Plus拥有庞大的用户社区和丰富的生态系统,提供了许多插件和扩展,具有丰富的第三方支持。
    • Spring Data JPA是Spring框架的一部分,与Spring集成紧密,与其他Spring组件无缝协作,享受Spring强大的功能和社区支持。

优缺点

MyBatis:
优点:

  1. 灵活性:MyBatis允许开发者编写原生SQL语句,具有更高的灵活性,并可以更好地优化SQL语句性能。
  2. 扩展性:MyBatis提供了许多插件和扩展机制,可以根据项目的需要进行定制和扩展。
  3. 易于学习和使用:MyBatis的学习曲线相对较低,配置简单,上手快。
  4. 轻量级:相比于其他ORM框架,MyBatis的依赖较少,对系统资源的消耗较小。

缺点:

  1. 编写大量的SQL语句:相比于ORM框架,MyBatis需要开发者手动编写和维护大量的SQL语句,对于复杂查询和关联查询需要更多的工作量。
  2. 开发效率较低:MyBatis需要编写大量的XML配置文件,增加了开发的工作量。

MyBatis Plus:
优点:

  1. 提供更多的便利功能:MyBatis Plus在MyBatis的基础上进行增强,提供了更多便利的功能,如代码生成器、通用Mapper、分页插件等。
  2. 减少重复性的开发:MyBatis Plus提供了常用的增删改查操作的封装,减少了重复性的开发工作。

缺点:

  1. 学习成本相对较高:相比于MyBatis,MyBatis Plus提供了更多的功能和特性,学习成本可能会略高。
  2. 过度封装的问题:MyBatis Plus的部分功能可能过度封装,不够灵活,无法满足一些复杂的需求。

Spring Data JPA:
优点:

  1. 提高开发效率:Spring Data JPA提供了一种更高级的抽象和简化的方式来操作数据库,可以通过方法命名约定自动生成常用的增删改查操作,减少了开发人员编写和维护SQL的工作量。
  2. 领域模型的支持:Spring Data JPA支持实体之间的关系映射、继承关系等高级语义,更适合于复杂业务需求和领域模型的开发。

缺点:

  1. 性能可能较低:相对于原生的SQL编写和优化,Spring Data JPA可能存在一些性能瓶颈,尤其是对于复杂查询和关联查询。
  2. 不够灵活:Spring Data JPA提供的抽象层可能不够灵活,对于一些特殊需求,需要使用原生的SQL语句进行操作。

本人在项目应用中最先接触的持久层框架是Mybatis,其支持xml文件并提供多种动态标签来写SQL语句,实现了灵活的数据处理方式。MyBatis采用#{}预编译来取代${}的传参方式,防止SQL注入危险(面试题)。Mybatis支持一级缓存、二级缓存。后来接触Mybatis Plus与Spring Data JPA,两者在使用上有些许类似。综上述说,如果你的项目业务不是很复杂,持久层框架推荐采用Spring Data JPA,它会让你的开发更加简单,代码更加简洁。

2Spring Boot集成

用啥研究啥(σ゚∀゚)σ…:*☆哎哟不错哦

2.1依赖

Spring Data JPA的依赖可以通过使用Spring Data Release Train BOM(Bill of Materials)或声明对Spring Data模块的依赖这两种方式来引入。

  1. 使用Spring Data Release Train BOM:
    • 这种方式是通过引入Spring Data Release Train BOM来管理Spring Data相关依赖的版本。BOM是一个包含了各个模块的版本信息的POM文件。
    • 通过在项目的pom.xml文件中添加对Spring Data Release Train BOM的依赖声明,可以自动管理Spring Data JPA以及其他Spring Data模块的版本一致性。
    • 通过BOM来统一管理版本,可以简化依赖管理的工作,确保不会发生不兼容或冲突的版本问题。
  2. 声明对Spring Data模块的依赖:
    • 这种方式是直接在项目的pom.xml文件中声明对具体Spring Data模块的依赖,比如Spring Data JPA。
    • 在这种方式下,需要明确指定所使用的Spring Data模块的版本号。
    • 这种方式更加灵活,可以精确控制所使用的每个Spring Data模块的版本以及依赖关系。

这两种方式都能够引入Spring Data JPA的依赖,主要区别在于版本管理的方式。如果你选择使用Spring Data Release Train BOM,可以更方便地管理和升级Spring Data相关模块的版本,减少版本冲突和不兼容性的问题。而通过声明对具体Spring Data模块的依赖,则可以更加精确地控制所使用的每个模块的版本。

我们采用方式2来依赖Spring Data JPA。我们在通过IDEA创建Spring Boot项目时候,在Dependencies->SQL中勾选上Spring Data JPA,发现引入的依赖为

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

而官网提供的依赖为

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
</dependency>

spring-boot-starter-data-jpaspring-data-jpa两者都是用于支持在Spring Boot项目中使用JPA(Java Persistence API)技术的依赖。它们的主要区别在于spring-boot-starter-data-jpa是一个Spring Boot的“启动器”,而spring-data-jpa是一个基本的依赖。下面是两者的区别:

  1. spring-boot-starter-data-jpa

    这是一个Spring Boot的启动器,它包含了一系列用于简化Spring Boot项目中使用JPA技术的依赖。启动器的作用是为了简化开发者的配置工作,它会自动地引入所需的库和配置。

    当你在项目中添加spring-boot-starter-data-jpa依赖时,它会自动包含以下依赖:

    • spring-data-jpa:用于支持Spring Data JPA的基本功能。
    • spring-orm:用于支持Spring的对象关系映射(Object-Relational Mapping, ORM)。
    • hibernate:作为默认的JPA实现。
    • spring-tx:用于支持事务管理。
    • 其他可能需要的依赖,如数据库连接池等。
  2. spring-data-jpa

    这是一个基本的依赖,提供了Spring Data JPA的核心功能。它包含了对JPA技术的支持,如实体管理、查询、事务处理等。如果你只想使用Spring Data JPA的基本功能,而不需要其他的自动配置,可以直接添加这个依赖。

使用spring-boot-starter-data-jpa,因为它可以帮助你简化配置和管理依赖,这也要求我们在启动项目时要添加数据库配置信息,我在此采用spring-boot-starter-data-jpa依赖。

2.2配置信息

如果没有添加数据源配置信息,启动会报一下错误,那么Spring Data JPA数据源的配置信息在哪里看呢[・_・?]

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-06-30 17:13:39.327 ERROR 29980 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

Disconnected from the target VM, address: '127.0.0.1:59605', transport: 'socket'

如果你也有上述问题那么说明你对Spring Boot项目掌握还是不够全面。

让我们一起读一下Spring Boot官网文档吧(https://docs.spring.io/spring-boot/docs/current/reference/html/documentation.html#documentation.data)。

该官网页面的目录是不是和IDEA创建Spring Boot项目时,选择Dependencies时的目录很相似。Spring Data JPA相关的信息当然看“6.Data“目录先查看。点击“SQL: Configuring a SQL Datastore, Embedded Database support, Connection pools, and more.”,再该页面可以查看具体的配置内容了,后面就请自行查阅吧。再次再补充一点,在application.properties文件中添加配置后,会发现所有的配置都来自于“package org.springframework.boot.autoconfigure”,该包的依赖为

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

该依赖是Spring Boot项目中常用的自动配置模块。它提供了一组默认的配置类,用于根据项目中的依赖和配置文件的设置,自动配置Spring Boot应用程序的行为。通过这个依赖,您可以快速搭建起一个基本的Spring Boot应用,并根据需要进行个性化的配置。

2.2.1数据源配置

顾名思义不解释。

spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_example
spring.datasource.username=springuser
spring.datasource.password=ThePassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

记得,使用哪个数据库,添加对应数据库驱动依赖。

2.2.2创建和删除JPA数据库

直接看package org.springframework.boot.autoconfigure.orm.jpa;下的

@ConfigurationProperties(prefix = "spring.jpa")
public class JpaProperties {...}

该类的成员属性都是配置项。

1. `spring.jpa.database`
   - 指定使用的数据库类型。
2. `spring.jpa.database-platform`
   - 指定使用的数据库方言(如何与特定数据库交互)。
3. `spring.jpa.defer-datasource-initialization`
   - 延迟初始化数据源,即在启动时不立即连接数据库。
4. `spring.jpa.generate-ddl`
   - 是否根据实体类生成数据库的DDL语句。
5. `spring.jpa.hibernate.ddl-auto`
   - 控制Hibernate在启动时如何处理数据库的DDL语句。在此配置中设置为create-drop,表示在应用程序启动时创建表,并在应用程序关闭时删除表。
6. `spring.jpa.hibernate.naming.implicit-strategy`
   - 设置Hibernate的隐式命名策略。在此配置中设置为org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl。
7. `spring.jpa.hibernate.naming.physical-strategy`
   - 设置Hibernate的物理命名策略。在此配置中设置为org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl。
8. `spring.jpa.hibernate.use-new-id-generator-mappings`
   - 是否启用新的ID生成器映射。
9. `spring.jpa.mapping-resources`
   - 额外的JPA映射资源文件。
10. `spring.jpa.open-in-view`
    - 启用OpenEntityManagerInView,即在视图渲染过程中延迟加载关联实体。
11. `spring.jpa.properties.asdf`
    - 自定义的JPA属性。
12. `spring.jpa.show-sql`
    - 是否在控制台上显示SQL语句。
13. `spring.data.jpa.repositories.bootstrap-mode`
    - JPA仓库的启动模式。
14. `spring.data.jpa.repositories.enabled`
    - 是否启用JPA仓库。

至此,可以正常启动Spring Boot项目了。

2.3CRUD说明

Spring Data存储库抽象中的中心接口是repository。此接口主要充当标记接口,用于捕获要使用的类型。CrudRepository和ListCrudRepository接口为被管理的实体类提供了复杂的CRUD功能。

IDEA双击shift,搜索一下该接口,复制出源码来,删除多余的注释:

package org.springframework.data.repository;

import java.util.Optional;

/**
 * Interface for generic CRUD operations on a repository for a specific type.
 *
 * @author Oliver Gierke
 * @author Eberhard Wolff
 * @author Jens Schauder
 */
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

	/**
	 * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
	 * entity instance completely.
	 *
	 * @param entity must not be {@literal null}.
	 * @return the saved entity; will never be {@literal null}.
	 * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
	 */
	<S extends T> S save(S entity);

	/**
	 * Saves all given entities.
	 *
	 * @param entities must not be {@literal null} nor must it contain {@literal null}.
	 * @return the saved entities; will never be {@literal null}. The returned {@literal Iterable} will have the same size
	 *         as the {@literal Iterable} passed as an argument.
	 * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is
	 *           {@literal null}.
	 */
	<S extends T> Iterable<S> saveAll(Iterable<S> entities);

	/**
	 * Retrieves an entity by its id.
	 *
	 * @param id must not be {@literal null}.
	 * @return the entity with the given id or {@literal Optional#empty()} if none found.
	 * @throws IllegalArgumentException if {@literal id} is {@literal null}.
	 */
	Optional<T> findById(ID id);

	/**
	 * Returns whether an entity with the given id exists.
	 *
	 * @param id must not be {@literal null}.
	 * @return {@literal true} if an entity with the given id exists, {@literal false} otherwise.
	 * @throws IllegalArgumentException if {@literal id} is {@literal null}.
	 */
	boolean existsById(ID id);

	/**
	 * Returns all instances of the type.
	 *
	 * @return all entities
	 */
	Iterable<T> findAll();

	/**
	 * Returns all instances of the type {@code T} with the given IDs.
	 * <p>
	 * If some or all ids are not found, no entities are returned for these IDs.
	 * <p>
	 * Note that the order of elements in the result is not guaranteed.
	 *
	 * @param ids must not be {@literal null} nor contain any {@literal null} values.
	 * @return guaranteed to be not {@literal null}. The size can be equal or less than the number of given
	 *         {@literal ids}.
	 * @throws IllegalArgumentException in case the given {@link Iterable ids} or one of its items is {@literal null}.
	 */
	Iterable<T> findAllById(Iterable<ID> ids);

	/**
	 * Returns the number of entities available.
	 *
	 * @return the number of entities.
	 */
	long count();

	/**
	 * Deletes the entity with the given id.
	 *
	 * @param id must not be {@literal null}.
	 * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}
	 */
	void deleteById(ID id);

	/**
	 * Deletes a given entity.
	 *
	 * @param entity must not be {@literal null}.
	 * @throws IllegalArgumentException in case the given entity is {@literal null}.
	 */
	void delete(T entity);

	/**
	 * Deletes all instances of the type {@code T} with the given IDs.
	 *
	 * @param ids must not be {@literal null}. Must not contain {@literal null} elements.
	 * @throws IllegalArgumentException in case the given {@literal ids} or one of its elements is {@literal null}.
	 * @since 2.5
	 */
	void deleteAllById(Iterable<? extends ID> ids);

	/**
	 * Deletes the given entities.
	 *
	 * @param entities must not be {@literal null}. Must not contain {@literal null} elements.
	 * @throws IllegalArgumentException in case the given {@literal entities} or one of its entities is {@literal null}.
	 */
	void deleteAll(Iterable<? extends T> entities);

	/**
	 * Deletes all entities managed by the repository.
	 */
	void deleteAll();
}

在这个接口中声明的方法通常被称为CRUD方法。ListCrudRepository提供了等价的方法,但是它们返回List,而CrudRepository方法返回Iterable。

除了CrudRepository之外,还有一个PagingAndSortingRepository抽象,它添加了额外的方法来简化对实体的分页访问:

public interface PagingAndSortingRepository<T, ID>  {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

标准CRUD功能存储库通常具有对底层数据存储的查询。使用Spring Data,声明使用这些查询变成了一个四步的过程:

  1. 声明一个接口继承Repository或它的一个子接口,并输入域类和它应该处理的ID类型,如下面的例子所示:

    interface PersonRepository extends Repository<Person, Long> { … }
    
  2. 在接口上声明查询方法。

    interface PersonRepository extends Repository<Person, Long> {
      List<Person> findByLastname(String lastname);
    }
    
  3. 设置Spring以使用JavaConfig或XML配置为这些接口创建代理实例。

    import org.springframework.data.….repository.config.EnableJpaRepositories;
    
    @EnableJpaRepositories
    class Config { … }
    
  4. 注入存储库实例并使用它,如下例所示:

    class SomeClient {
    
      private final PersonRepository repository;
    
      SomeClient(PersonRepository repository) {
        this.repository = repository;
      }
    
      void doSomething() {
        List<Person> persons = repository.findByLastname("Matthews");
      }
    }
    

Y(o)Y好了,按照这四部就可以实现CRUD操作了,但是这里有个疑问:域类(domain class )与ID是什么意思?如果你曾经使用过Hibernate,你大概就知道了domain概念的含义。

2.3JPA Domain

Hibernate:https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#domain-model

domain模型来自数据建模领域。它是最终描述您正在处理的问题域的模型。有时您还会听到persistent classes这个术语。通过Hibernate提供的注解,实现domain class与数据库表的映射,下面举例了一个简单表与domain model(class)的映射实现。

  • DDL:

    create table Contact (
        id integer not null,
        first varchar(255),
        last varchar(255),
        middle varchar(255),
        notes varchar(255),
        starred boolean not null,
        website varchar(255),
        primary key (id)
    )
    
  • domain model:

    @Entity(name = "Contact")
    public static class Contact {
    
    	@Id
    	private Integer id;
    
    	private Name name;
    
    	private String notes;
    
    	private URL website;
    
    	private boolean starred;
    
    	//Getters and setters are omitted for brevity
    }
    
    @Embeddable
    public class Name {
    
    	private String firstName;
    
    	private String middleName;
    
    	private String lastName;
    
    	// getters and setters omitted
    }
    

Y(o)Y好了,简单说明一下概念,想知道更多直接看官网吧。

2.4CRUD实操

  1. 创建Student domain class

    package com.example.springdatajpademo.model.po;
    
    /**
     * @Author yrz
     * @create 2023/7/1 9:37
     * @Description TODO
     */
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "students")
    @Setter
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public class Student {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(name = "name")
        private String name;
    
        @Column(name = "age")
        private Integer age;
    
        @Column(name = "gender")
        private Integer gender;
    
        @Column(name = "class_id")
        private String classId;
    
        @Column(name = "discipline_id")
        private String disciplineId;
    }
    
    

    编写完domain class后启动项目,自动创建数据库表。

在这里插入图片描述

  1. 声明接口以及查询方法

    package com.example.springdatajpademo.dao;
    
    
    import com.example.springdatajpademo.model.po.Student;
    import org.springframework.data.repository.CrudRepository;
    /**
     * @Author yrz
     * @create 2023/7/3 9:50
     * @Description
     * 这里我继承了CrudRepository,它为您提供了CRUD功能的方法。除此之外还可以继承ListCrudRepository、ReactiveCrudRepository(响应式存储)、
     * RxJava3CrudRepository(响应式存储)、CoroutineCrudRepository(Kotlin)、PagingAndSortingRepository、CoroutineSortingRepository,
     * 如果不想扩展Spring Data接口,还可以用@RepositoryDefinition注释存储库接口。
     */
    
    public interface SpringDataJpaTestDao extends CrudRepository<Student, Long> {
    
        /**
         * 自定义方法
         * 根据名称查询学生
         * @param name
         * @return
         */
        Student findByName(String name);
    
        /**
         * 根据名称和性别查询
         * @param name
         * @param gender
         * @return
         */
        Student findByNameAndGender(String name, Integer gender);
    
        /**
         * 根据名称和性别查询
         * 无法解析该方法名称,程序报错
         * @return
         */
    //    Student findByMingChengAndXingBie(String mingCheng, Integer xingBie);
    //    Student findByMingChengAndXingBie(String name, Integer gender);
    }
    
    package com.example.springdatajpademo;
    
    import com.alibaba.fastjson2.JSON;
    import com.example.springdatajpademo.dao.SpringDataJpaTestDao;
    import com.example.springdatajpademo.model.po.Student;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.util.Iterator;
    import java.util.Optional;
    
    @SpringBootTest
    @Slf4j
    class SpringDataJpaDemoApplicationTests {
    
        @Autowired
        private SpringDataJpaTestDao springDataJpaTestDao;
    
        @Test
        void contextLoads() {
    
        }
    
        @Test
        public void testFind(){
            // 查全部
            Iterable<Student> all = springDataJpaTestDao.findAll();
            Iterator<Student> iterator = all.iterator();
            while (iterator.hasNext()){
                Student next = iterator.next();
                log.info("findAll:{}", JSON.toJSONString(next));
            }
            // 根据ID查询
            Optional<Student> byId = springDataJpaTestDao.findById(1L);
            Student student = byId.get();
            log.info("findById:{}", JSON.toJSONString(student));
            // 根据名称查询
            Student byName = springDataJpaTestDao.findByName("李四");
            log.info("findByName:{}",JSON.toJSONString(byName));
            // 新增
    //        Student student1 = new Student(5L,"张三",19,0,"1234","3");
    //        Student save = springDataJpaTestDao.save(student1);
    //        log.info("save:{}",JSON.toJSONString(save));
            // 根据名称和性别查询
            Student student2 = springDataJpaTestDao.findByNameAndGender("张三", 0);
            log.info("findByNameAndGender:{}",JSON.toJSONString(student2));
    //        Student student3 = springDataJpaTestDao.findByMingChengAndXingBie("张三", 1);
    //        log.info("findByMingChengAndXingBie:{}",JSON.toJSONString(student3));
        }
    
    }
    
    Hibernate: select student0_.id as id1_0_, student0_.age as age2_0_, student0_.class_id as class_id3_0_, student0_.discipline_id as discipli4_0_, student0_.gender as gender5_0_, student0_.name as name6_0_ from students student0_
    2023-07-03 11:00:25.198  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findAll:{"age":18,"classId":"321465","disciplineId":"1","gender":1,"id":1,"name":"张三"}
    2023-07-03 11:00:25.198  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findAll:{"age":18,"classId":"321546648","disciplineId":"1","gender":1,"id":2,"name":"李四"}
    2023-07-03 11:00:25.198  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findAll:{"age":18,"classId":"123","disciplineId":"1","gender":0,"id":3,"name":"王五"}
    2023-07-03 11:00:25.198  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findAll:{"age":19,"classId":"1234","disciplineId":"3","gender":0,"id":4,"name":"张三"}
    Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.class_id as class_id3_0_0_, student0_.discipline_id as discipli4_0_0_, student0_.gender as gender5_0_0_, student0_.name as name6_0_0_ from students student0_ where student0_.id=?
    2023-07-03 11:00:25.214  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findById:{"age":18,"classId":"321465","disciplineId":"1","gender":1,"id":1,"name":"张三"}
    Hibernate: select student0_.id as id1_0_, student0_.age as age2_0_, student0_.class_id as class_id3_0_, student0_.discipline_id as discipli4_0_, student0_.gender as gender5_0_, student0_.name as name6_0_ from students student0_ where student0_.name=?
    2023-07-03 11:00:25.258  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findByName:{"age":18,"classId":"321546648","disciplineId":"1","gender":1,"id":2,"name":"李四"}
    Hibernate: select student0_.id as id1_0_, student0_.age as age2_0_, student0_.class_id as class_id3_0_, student0_.discipline_id as discipli4_0_, student0_.gender as gender5_0_, student0_.name as name6_0_ from students student0_ where student0_.name=? and student0_.gender=?
    2023-07-03 11:00:25.263  INFO 31840 --- [           main] c.e.s.SpringDataJpaDemoApplicationTests  : findByNameAndGender:{"age":19,"classId":"1234","disciplineId":"3","gender":0,"id":4,"name":"张三"}
    2023-07-03 11:00:25.287  INFO 31840 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
    2023-07-03 11:00:25.293  INFO 31840 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
    Disconnected from the target VM, address: '127.0.0.1:62235', transport: 'socket'
    2023-07-03 11:00:25.309  INFO 31840 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
    

    Spring Data JPA的默认查找策略是CreateIfNotFound,参阅“3.创建配置类编写代理实例”。通俗的讲就是它可以通过在继承Repository等接口的接口中自定义方法,然后方法名解析出该方法对应的SQL。下面是官网对于解析方法的一些说明,当我们在使用自定义持久层方法时,如果你想解放双手不手写SQL,也需要按照下面的说明命名方法。

    4.4.2. Query Creation

    解析查询方法名分为主语和谓语。第一部分(find…By, exists…By)定义查询的主题,第二部分形成谓词。引子句(主语)可以包含进一步的表达。find(或其他引入关键字)和By之间的任何文本都被认为是描述性的,除非使用结果限制关键字之一,如Distinct来在要创建的查询上设置不同标志,或使用Top/First来限制查询结果。

    附录包含查询方法主题关键字和查询方法谓词关键字的完整列表,包括排序和大小写修饰符。但是,第一个By充当分隔符,以指示实际条件谓词的开始。在非常基本的级别上,您可以定义实体属性上的条件,并将它们与And和Or连接起来。

    解析方法的实际结果取决于为其创建查询的持久性存储。然而,有一些一般的事情需要注意:

    表达式通常是属性遍历和可以连接的操作符的组合。可以将属性表达式与“AND”和“OR”结合使用。对于属性表达式,还支持Between、LessThan、GreaterThan和Like等操作符。受支持的操作符因数据存储而异,因此请参阅参考文档的相应部分。

    方法解析器支持为单个属性(例如,findByLastnameIgnoreCase(…))或支持忽略大小写的类型的所有属性(通常是字符串实例-例如,findByLastnameAndFirstnameAllIgnoreCase(…))设置IgnoreCase标志。是否支持忽略大小写可能因存储而异,因此请参阅参考文档中的相关章节,了解特定于存储的查询方法。

    可以通过向引用属性的查询方法追加OrderBy子句和提供排序方向(Asc或Desc)来应用静态排序。要创建支持动态排序的查询方法,请参见“Paging, Iterating Large Results, Sorting”。

    关于这部分更多内容,请参阅官网文档。

  2. 创建配置类编写代理实例

    当您使用Spring Data JPA时,您需要定义一个继承自JpaRepository或其子接口的接口,用于定义对数据库的操作。@EnableJpaRepositories注解的作用是告诉Spring启用JPA仓库的自动配置,并扫描指定的包或类路径,寻找这些定义的JPA仓库接口。

    在使用@EnableJpaRepositories注解时,通常需要指定basePackages属性或value属性,以声明要扫描的包或类路径。这样,Spring就会自动查找该包或类路径下所有继承自JpaRepository或其子接口的接口,并自动生成对应的实现类。

    Spring Boot中使用Spring Data JPA时,不写@EnableJpaRepositories注解和其他相关配置注解也是可以的。

    Spring Boot默认会自动配置JPA仓库,包括使用@EnableJpaRepositories注解启用JPA仓库的自动配置。

    当您使用Spring Boot的默认约定和项目结构时,它会自动扫描主应用程序类所在的包以及其子包中的所有@Entity注解的实体类和@Repository注解的仓库接口。然后,Spring Boot会自动生成和注册这些JPA仓库的实现类。

    只有当您的项目结构不符合Spring Boot的默认约定,或者您想自定义JPA仓库的配置时,才需要显式地使用@EnableJpaRepositories注解和其他相关配置注解。

    在绝大多数情况下,您不需要显式地添加@EnableJpaRepositories注解和其他相关配置注解,Spring Boot会自动为您配置和管理JPA仓库。

    @EnableJpaRepositories注解提供了一些可配置的属性,用于自定义JPA仓库的行为。以下是一些常用的属性及其含义:

    1. basePackages: 指定要扫描的包路径,以查找继承自JpaRepository或其子接口的JPA仓库接口。可以通过字符串数组的形式指定多个包路径。

    2. value: 与basePackages属性作用相同,可以指定要扫描的包路径。

    3. basePackageClasses: 指定含有JPA仓库接口的类,Spring会根据这些类的包路径进行扫描。可以通过类数组的形式指定多个类。

    4. entityManagerFactoryRef: 指定要使用的EntityManagerFactory bean的名称。如果没有指定,默认会使用一个默认的EntityManagerFactory bean。

    5. transactionManagerRef: 指定要使用的PlatformTransactionManager bean的名称。如果没有指定,默认会使用一个默认的PlatformTransactionManager bean。

    6. repositoryImplementationPostfix: 定义自动生成的JPA仓库实现类的后缀。默认值为Impl,例如,对于UserRepository接口,自动生成的实现类的名称为UserRepositoryImpl

    7. namedQueriesLocation: 指定在类路径下的位置,用于加载命名查询(Named Queries)。可以是一个文件路径、类路径或URL。

    8. repositoryFactoryBeanClass: 指定用于创建JPA仓库的工厂类。默认为org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean

    9. queryLookupStrategy属性用于指定JPA仓库查询方法的查找策略。可以通过设置不同的值来改变默认行为。

      以下是queryLookupStrategy属性可用的选项:

      • Create: 直接根据方法名查询, 如果方法名不符合规则, 抛出异常, 是在项目启动的时候就会抛出异常。
      • UseDeclaredQuery: 注解方式, 又叫声明方式。 通过接口方法上面配置的注解查询, 注解里面SQL写错了, 启动的时候也会报错。
      • CreateIfNotFound: 上面两种方式的综合体, 先通过注解找, 找不到就通过方法名找(默认)。

2.5自定义SQL

尽管从方法名获取查询非常方便,但可能会遇到这样的情况:方法名解析器不支持想要使用的关键字,或者方法名会变得不必要地难看。因此,您可以通过命名约定使用JPA命名查询,也可以使用@Query注释您的查询方法(请参阅使用Using @Query了解详细信息)。

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

2.5.1nativeQuery

在Spring Data JPA中,@Query注解用于指定自定义的JPQL(Java Persistence Query Language)或SQL查询。nativeQuery参数用于指示是否使用原生的SQL查询,可以设置为truefalse

  • nativeQuerytrue时,表示使用原生的SQL查询。这意味着可以直接编写SQL语句,而不是JPQL语句。在这种情况下,查询语句将会被传递给底层数据库进行执行,可以使用SQL特有的语法和函数。这对于复杂的查询或需要使用数据库特定功能的情况非常有用。

以下是使用nativeQuerytrue的示例:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    
    
    @Query(value = "SELECT * FROM users WHERE username = ?1", nativeQuery = true)
    User findUserByUsername(String username);
}

在上面的示例中,我们使用了nativeQuery = true来指定查询使用原生的SQL语句来查询数据库中的users表。在执行查询时,Spring Data JPA会将该查询语句传递给数据库执行,并将结果映射到User对象。

  • nativeQueryfalse时(默认值),表示使用JPQL查询。JPQL是一种面向对象的查询语言,与实体类和数据库字段进行交互。在这种情况下,查询语句将会由Spring Data JPA解释和转换为底层数据库支持的SQL,然后执行查询。JPQL提供了一些与独立于数据库的对象模型交互的功能。

以下是使用nativeQueryfalse的示例(默认值):

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    
    
    @Query("SELECT u FROM User u WHERE u.username = ?1")
    User findUserByUsername(String username);
}

在上述示例中,我们使用了JPQL语句SELECT u FROM User u WHERE u.username = ?1来查询数据库中的User对象。

总结起来,使用nativeQuerytrue表示使用原生的SQL查询,而nativeQueryfalse表示使用JPQL查询。选择使用哪种查询取决于具体的需求,包括查询复杂度、数据库特定功能等。

2.5.2传参方式

@Query注解定义的SQL查询可以通过多种方式传递参数。下面是几种常见的传参方式:

  1. 位置参数(Positional Parameters):使用?占位符表示参数,按照参数在方法中声明的顺序传递。示例:

    @Query("SELECT u FROM User u WHERE u.id = ?1")
    User findUserById(Long id);
    

    在上面的例子中,?1表示第一个位置的参数,即方法的id参数。相应地,你可以使用?2?3等表示后续参数。

  2. 命名参数(Named Parameters):使用:前缀给参数命名,然后在查询语句中使用该命名参数。示例:

    @Query("SELECT u FROM User u WHERE u.username = :username")
    User findUserByUsername(@Param("username") String username);
    

    在上述示例中,username是一个命名参数,使用@Param注解指定其值是方法的username参数。

    如果@Query注解中的WHERE条件有多个,并且其中一个值可能为null,你可以使用动态查询来保证SQL正确执行。

    一种常见的方法是使用JPQL的条件表达式(https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#hql)来构建动态查询,根据条件是否为null来决定是否添加相应的查询条件。下面是一个示例:

    @Repository
    public interface UserRepository extends JpaRepository<User, Long> {
          
          
        
        @Query("SELECT u FROM User u WHERE (:username is null or u.username = :username) AND (:age is null or u.age = :age)")
        List<User> findUsersByUsernameAndAge(@Param("username") String username, @Param("age") Integer age);
    }
    

    在上述示例中,我们通过使用条件表达式和空值的判定来构建了动态查询。在查询语句中,通过使用:username is null or u.username = :username:age is null or u.age = :age的方式,判断了usernameage是否为null,如果是null则忽略该条件,否则添加相应的查询条件。

    通过这种方式,即使其中一个条件的值为null,查询仍然能够正常执行,不会影响查询的结果。

    当使用动态查询时,还可以根据具体的需求进行更复杂的判断和组合条件。你可以根据实际情况调整查询语句,添加更多的条件和逻辑判断来满足具体的查询需求。

    通过使用动态查询和条件表达式,可以解决@Query注解中的WHERE条件有多个且其中一个值为null的情况,确保SQL查询的正确执行。

  3. SpEL表达式参数(SpEL Expression Parameters):使用#{}将SpEL表达式嵌入查询语句中。SpEL表达式可以引用方法参数、类属性、静态常量等。示例:

    @Query("SELECT u FROM User u WHERE u.age > :#{#minAge ?: 18}")
    List<User> findByAgeGreaterThan(@Param("minAge") Integer minAge);
    

    上面的例子中,SpEL表达式#{#minAge ?: 18}表示取minAge参数的值作为查询中的最小年龄,在没有提供minAge参数时,默认使用18作为最小年龄。

2.5.3EntityManager

除了上述两种(解析方法名、@Query)方法定义SQL外,还可以通过EntityManager来执行自定义的SQL。

在Spring Data JPA中,EntityManager是JPA(Java Persistence API)的核心接口之一,用于管理实体对象和执行与持久化相关的操作。

EntityManager的主要作用如下:

  1. 实体管理:EntityManager负责跟踪、管理和操作实体对象的生命周期。它可以创建、读取、更新和删除实体对象。可以使用EntityManager的persist、find、merge和remove等方法来执行这些操作。

  2. 数据库事务管理:EntityManager在事务管理方面起着重要的作用。通过EntityManager可以启动、提交或回滚数据库事务。在Spring Data JPA中,事务管理通常由Spring框架提供,但EntityManager是与事务管理密切相关的一部分。

  3. 缓存管理:EntityManager使用一级缓存来提高性能。一级缓存存储已经查询或持久化的实体对象,以减少对数据库的访问。通过实体对象的标识符,可以快速检索缓存中的实体对象,从而避免对数据库的不必要查询。

  4. 查询执行:EntityManager可以执行查询语句,包括JPQL查询和原生SQL查询。它提供了createQuery、createNamedQuery和createNativeQuery等方法,用于创建并执行查询。

  5. 实体关系管理:EntityManager支持实体之间的关系映射和管理。它可以处理实体之间的关联关系,包括一对一、一对多、多对一和多对多等关系。通过EntityManager,可以加载和操作关联的实体对象。

需要注意的是,在Spring Data JPA中,EntityManager是由JpaRepository接口和实现类(如SimpleJpaRepository)隐式地创建和使用的。你通常不需要直接使用EntityManager来执行基本的CRUD操作,而是使用JpaRepository提供的方法。

我们可以调用createNativeQuery()方法来执行SQL语句,这在项目中也有一定的应用场景,比如对同一组查询结果进行avg、max等不同的聚合操作时候,我们可以通过字符串拼接SQL,这样只需要替换SQL的聚合函数那部分的字符串即可。总之为了少些SQL,EntityManager能提供更加灵活的方法。

3与Hibernate的关系

Spring Data JPA是对JPA(Java Persistence API)规范的实现,而Hibernate是JPA规范的一个具体实现。

JPA是Java领域的一种持久化标准,定义了一系列用于对象关系映射(ORM)的接口和规则。它提供了一套API和标准注解,用于将Java实体类映射到关系型数据库。

Hibernate是一个优秀的ORM框架,它实现了JPA规范,并提供了更多的功能和特性。Hibernate提供了丰富的ORM功能,包括对象关系映射、事务管理、查询语言和缓存等。

Spring Data JPA是Spring框架中的一个模块,它提供了一种简化的方式来访问和操作数据库,基于JPA规范实现了一套简洁的CRUD(Create, Retrieve, Update, Delete)接口。Spring Data JPA封装了Hibernate等ORM框架的底层细节,提供了更加便捷的持久化操作。

因此,Spring Data JPA使用了Hibernate作为其默认的JPA实现提供商,通过使用Spring Data JPA,您可以更轻松地使用Hibernate进行数据库访问和操作。同时,Spring Data JPA还支持其他JPA实现提供商,如EclipseLink等,使得您可以更容易地切换和替换不同的JPA实现。

4结语

关于Spring Data JPA的更多使用技巧以及技术点的介绍会在后续的日常使用中进行补充,如果你觉得其哪个技术点在项目中很实用,欢迎评论并一起讨论。

猜你喜欢

转载自blog.csdn.net/qq_27890899/article/details/131548675