SpringBoot-2-Jpa

作者: 行者N | 来源:发表于2017-08-22 09:06 被阅读666次

    2017年8月21日 我原本只想简单记录一下springboot中应用Jpa的简单操作。不想由于hibernate忘记太多了,不停的在查找记录,于是就有了本文的复杂版本,感觉要写清楚的东西太多了,都想分几篇文章来写清楚。限于表达,至少最后我自己能看明白。

    SpringBoot 使用Jpa是官方推荐,一般使用hibernate实现的Jpa。配置步骤如下

    pom配置

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

    application.properties

    #jpa
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
    spring.jpa.show-sql= true
    

    简单配置这些就够了,其他的是属于数据源的配置,这里也记录下来,方便使用吧

    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot-demo?useUnicode=true&characterEncoding=utf8
    spring.datasource.username=root
    spring.datasource.password=xxxxx
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    

    书写Entity

    Entity是Jpa实现的一个比较重要的部分,详细说明如下

    Entity基本格式

    借用hibernate实现,所以注释基本上与hibernate相同。@Entity标签表面是个Jpa识别的Entity,@Table 表明对应的表
    字段,如果与数据库字段一致,则不用增加任何标记。如果不一致,增加@Column标签

    这里注意:jpa默认的是@Column标签的名字与字段名字一样,然后解析这个名字的时候,按照驼峰命名法去解析,既userName 会给解析成 user_name,如果在数据库中的字段就是userName,则@Column(name = "username"),

    这里可以考虑策略,还可以在属性文件中这样定义,

    spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    
    @Entity
    @Table(name = "sys_user")
    public class SysUser  implements Serializable {
        private static final long serialVersionUID = -3962204755050776389L;
        @Id
        private Integer id;
        private String password;
        private String phone;
        private String email;
        @Column(name = "enterprise_style")
        private String enterpriseStyle;
      
    //省略 get set
    }
    

    常用的一些标签

    @JsonIgnore

    @JsonIgnore 的作用是json序列化时将java bean中的一些属性忽略掉,序列化和反序列化都受影响。当表间有One2Many或Many2One时,会发生无限循环的场景,如何破?只要在Set方法前增加以下注解即可

    @JsonIgnore  
        public Set xxxs() {
            return this.xxxYyyy;
        }
    

    @Transient

    @Transient表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性;
    如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,否则ORM框架默认其注解为@Basic;

    表示该字段在数据库表中没有,但是一定要标记在getXXX方法上。特别是bean可以修改数据库表的时候。

    @Transient
    public int getAge() {
     return 1+1;
    }
    

    @JsonIgnoreProperties

    @JsonIgnoreProperties 此注解是类注解,作用是json序列化时将java bean中的一些属性忽略掉,序列化和反序列化都受影响。

    @JsonIgnore

    此注解用于属性或者方法上(最好是属性上),作用和上面的@JsonIgnoreProperties一样。

    @JsonFormat

    此注解用于属性或者方法上(最好是属性上),可以方便的把Date类型直接转化为我们想要的模式,比如@JsonFormat(pattern = "yyyy-MM-dd HH-mm-ss")

    @JsonSerialize

    此注解用于属性或者getter方法上,用于在序列化时嵌入我们自定义的代码,比如序列化一个double时在其后面限制两位小数点。

    @JsonDeserialize

    此注解用于属性或者setter方法上,用于在反序列化时可以嵌入我们自定义的代码,类似于上面的@JsonSerialize

    这一部分可以参见 https://www.cnblogs.com/softidea/p/5668697.html

    Id 策略

    @GeneratedValue:主键的产生策略,通过strategy属性指定
    主键产生策略通过GenerationType来指定。GenerationType是一个枚举,它定义了主键产生策略的类型。

    1、AUTO 自动选择一个最适合底层数据库的主键生成策略。如MySQL会自动对应auto increment。这个是默认选项,即如果只写@GeneratedValue,等价于@GeneratedValue(strategy=GenerationType.AUTO)。

    2、IDENTITY 表自增长字段,Oracle不支持这种方式。

    3、SEQUENCE 通过序列产生主键,MySQL不支持这种方式。

    4、TABLE 通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。不同的JPA实现商生成的表名是不同的,如 OpenJPA生成openjpa_sequence_table表,Hibernate生成一个hibernate_sequences表,而TopLink则生成sequence表。这些表都具有一个序列名和对应值两个字段,如SEQ_NAME和SEQ_COUNT。
    如果使用Hibernate对JPA的实现,可以使用Hibernate对主键生成策略的扩展,通过Hibernate的@GenericGenerator实现。

    @GenericGenerator(name = “system-uuid”, strategy = “uuid”) 声明一个策略通用生成器,name为”system-uuid”,策略strategy为”uuid”。

    @GeneratedValue(generator = “system-uuid”) 用generator属性指定要使用的策略生成器。

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import org.hibernate.annotations.GenericGenerator;
     
    @Entity
    @Table(name="csq_test")
    public class Test {
        private Long id;
        @Id
        @GenericGenerator(name="idGenerator", strategy="uuid") //这个是hibernate的注解/生成32位UUID
        @GeneratedValue(generator="idGenerator")
        public Long getId() {
            return id;
        }
        public void setId(Long id) {
            this.id = id;
        }
    }
    

    映射

    举例说明,在公司权限中会存在,公司,部门,员工,其中一个部门中有多个员工,公司有多个部门,类似于这种。
    这些映射中都可以使用下面的参数

    fetch=FetchType.LAZY为默认的数据延迟加载,fetch=FetchType.EAGER为急加载

    cascade={CascadeType.PERSIST,CascadeType.MERGE,
    CascadeType.REFRESH,CascadeType.REMOVE}
    其中:
    CascadeType.PERSIST级联新增(又称级联保存);
    CascadeType.MERGE:级联合并(级联更新);
    CascadeType.REMOVE:级联删除;
    CascadeType.REFRESH:级联刷新
    CascadeType.ALL:以上四种都是;
    一般采用CascadeType.MERGE:级联合并(级联更新)即可。默认值是均不进行关联。

    增加默认的查询条件可以在Set上增加@Where(clause="status = 1")

    OneToOne

    在一对一的关系中,只需在主控方内注明@OneToOne,而被控方只是作为外键,不需任何特殊标记

    @OneToOne  
    @JoinColumn(name="ACCOUNT_ID")  
    private AccountEntity account;  
    

    对应的员工表上,存储账户表的外键就可以了

    image.png

    在另一端如果想使用的话可以

    @OneToOne(mappedBy="account")  
    private EmployeeEntity employee;  
    

    这样维护的工作就都由employee来完成了。即保存employee时,会同时保存account

    OneToMany && ManyToOne

    公司(组织)表相对于部门表是被控方,只需在被控方写mappedBy,其值为主控方中引用的外键对象的名称

    @Entity
    @Table(name = "costume_organization")
    public class Organization extends AbstractEntity {
        private static final long serialVersionUID = 1L;
    
        @Column(nullable = false, length = 50)
        private String name; // 组织名称
    
        @OneToMany(mappedBy = "organization")
        private Set<Department> departmentSet; // 部门集合
    }
    

    部门表相对于公司(组织)表是主控方,在主控方只需写@ManyToOne即可,其对象名为被控表中mappedBy中的值。如果需要将公司表对象或者部门表对象转为JSON数据的时候,为了防止出现无限循环包含对方,需要在部门表(多的一方)的引用公司表(少的一方)对象的set方法上写上注解@JsonBackReference(或者,在对应的一方的bean上,写上@JsonIgnore)

    @Entity
    @Table(name = "costume_department")
    public class Department extends AbstractEntity {
        private static final long serialVersionUID = 1L;
    
        @Column(nullable = false, length = 50)
        private String name; // 部门名称
    
        
        private Organization organization; // 组织外键
    @ManyToOne(optional = false)//这个放get上,放字段上有时候不好用
        getOrganization(){
    }
        @ManyToMany
        private Set<Member> memberSet; // 用户表外键
    
        public Organization getOrganization() {
            return organization;
        }
    
        @JsonBackReference
        public void setOrganization(Organization organization) {
            this.organization = organization;
        }
    }
    

    ManyToMany

    @ManyToMany 注释:表示此类是多对多关系的一边,mappedBy 属性定义了此类为双向关系的维护端,注意:mappedBy 属性的值为此关系的另一端的属性名。
    例如,在Student类中有如下方法:
    被控方:

    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "students")
    public Set<Teacher> getTeachers() {
    return teachers;
    }
    

    那么这里的“students”就是Teachers的一个属性,通常应该是这样的:
    Set<Student> students;
    另一端的getStudents方法如下所示:
    主控方:

    @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
    @JoinTable(name = "Teacher_Student",
    joinColumns = {@JoinColumn(name = "Teacher_ID", referencedColumnName = "teacherid")},
    inverseJoinColumns = {@JoinColumn(name = "Student_ID", referencedColumnName ="studentid")})
    public Set<Student> getStudents() {
    return students;
    }
    

    @ManyToMany 注释表示Teacher 是多对多关系的一端。@JoinTable 描述了多对多关系的数据表关系。name 属性指定中间表名称,joinColumns 定义中间表与Teacher 表的外键关系。上面的代码中,中间表Teacher_Student的Teacher_ID 列是Teacher 表的主键列对应的外键列,inverseJoinColumns 属性定义了中间表与另外一端(Student)的外键关系。

    可以通过上面的定义看到有三个表学生表--老师表--老师学生中间表
    以上提到主控方和被控方。。有人不赞同这种写法:
    理由是:
    1.既然是多对多关系。。为什么还要分主动方和被动方? 2.为什么需要删除老师后才级联中间表。。。请注意:以上定义方法时,删除学生是无法级联删除中间表的。
    正确的写法应该是两边都用主控方的写法:只是joinColumns和inverseJoinColumns属性的地方互换就可以了。

    @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
    @JoinTable(name = "Teacher_Student",
    joinColumns = {@JoinColumn(name = "Student_ID", referencedColumnName = "studentid")},
    inverseJoinColumns = {@JoinColumn(name = "Teacher_ID", referencedColumnName ="teacherid")})
    public Set<Teacher> getTeachers() {
    return teachers;
    }
    
    @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
    @JoinTable(name = "Teacher_Student",
    joinColumns = {@JoinColumn(name = "Teacher_ID", referencedColumnName = "teacherid")},
    inverseJoinColumns = {@JoinColumn(name = "Student_ID", referencedColumnName ="studentid")})
    public Set<Student> getStudents() {
    return students;
    }
    

    注意

    这个可以不使用mappyBy,但是一定是一方是fetch = FetchType.LAZY,如果两方都是 fetch = FetchType.EAGER 系统不能启动,会报错,特别说明一下。

    Jpa对象的继承映射策略

    在企业级开发中,经常会碰到的一种对象是这样的,实体公司、部门、人员,都会有ID、code、name等公共属性,本着面向对象的处理方法,在java世界里,一般都会抽象出一层来,比如称为 BaseBean,或者 BaseEntity,这样第一可以通过base来统一处理通用的CURD,第二,通过 extends BaseEntity 可以少写很多代码。
    这样的逻辑在Jpa这边如何处理呢?其实也可以转换一下,这种关系如何反应到数据库上?
    这个问题从头说,一般来说实体和表的映射关系分为3种

    • Single-table :这是继承映射中的缺省策略,在不特别指明的情况下,系统默认就是采用这种映射策略进行映射的。这个策略的映射原则就是父类包括子类中新添加的属性全部映射到一张数据库表中,数据库表中有一个自动生成的字段用来存储区分不同的子类的信息
    • Joined-subclass:这种映射策略中,继承关系中的每一个实体类,无论是具体类 (concrete entity) 或者抽象类 (abstract entity),数据库中都有一个单独的表与他对应。子实体对应的表中不含有从根实体继承而来的属性,它们之间通过共享主键的方式进行关联。
    • Table-per-concrete-class :这个策略就是将继承关系中的每一个实体映射到数据库中的一个单独的表中,与“Joined”策略不同的是,子实体对应的表中含有从根实体继承而来的属性。

    这一部分是抄来的,就抄到底吧(脸红一下下),因为原文写的实在是好,这样省得有误解,来源已经标明在文章的最后。

    下文举例种会应用的例子的UML图


    image.png

    Single Table

    单表(Single-Table)映射是继承映射中的缺省映射策略,在不加说明的情况下,也就是不在根实体 (root entity) 中指定映射策略的时候默认就是使用的这种映射策略。在本例中根实体 (root entity) 指的是实体 Item 类。
    Item 实体类没有用 @Inheritance 这个注解 (annotation) 进行注释,说明在这个继承关系中使用的是单表映射策略。同时 Item 实体类是 Phone 实体类和 Magazine 实体类的根实体 (root entity),它们继承了 Item 的属性并且拥有自己的独有的属性。

    @Entity 
    public class Item implements Serializable { 
       private static final long serialVersionUID = 1L; 
       @Id 
       @GeneratedValue(strategy = GenerationType.AUTO) 
       private Long id; 
       private String title; 
       private Float price; 
       private String decription; 
      // Getters and Setters 
    }
    @Entity 
    public class Phone extends Item { 
       private String factory; 
       private Float DurationTime; 
        // Getters and Setters 
    }
    @Entity 
    public class Magazine extends Item { 
       private String isbn; 
       private String publisher; 
        // Getters and Setters 
    }
    

    对应的ER图

    image.png

    对应的数据

    image.png

    单表映射中除了将所有子类中的属性和父类集中在同一个表中之外,还多添加了一个 DTYPE 的列,这个列主要是为了区别子类的,他的缺省属性是 String 类型,缺省值就是子实体 (entity) 类的类名。数据库中记录的一个片断,DTYPE 列的值默认就是类的名字,它是用来区分本行的记录属于继承关系的中的哪个类的。DTYPE 的值是 Item 的行,就是由根实体持久化而来的,以此类推。
    用来区分子类的字段也是可以自定义的,如下:

    @Entity 
    @DiscriminatorColumn(name="DISC",discriminatorType=DiscriminatorType.CHAR) 
    @DiscriminatorValue("I") 
    public class Item implements Serializable { 
     
       private static final long serialVersionUID = 1L; 
       @Id 
       @GeneratedValue(strategy = GenerationType.AUTO) 
       private Long id; 
       private String title; 
       private Float price; 
       private String decription; 
     
        // Getters and Setters 
     
    }
    @Entity 
    @DiscriminatorValue("M") 
    public class Magazine extends Item { 
     
       private String isbn; 
       private String publisher; 
     
       // Getters and Setters 
     
    }
    @Entity 
    @DiscriminatorValue("P") 
    public class Phone extends Item { 
     
       private String factory; 
       private Float DurationTime; 
     
        // Getters and Setters 
     
    }
    

    这样定义后,数据库实际如下

    image.png

    这种策略对实体之间的多种关联关系能提供很好的支持,同时在查询方面也有很好的效率。但是这种映射策略在数据库表中会有很多的空字段的存在。这样势必会造成数据库资源的大量浪费,同时这个映射策略也要求子类中的所有属性也必须是可空 (null able) 的。

    Joined-subclass

    这种映射策略,继承结构中的每一个实体 (entity) 类都会映射到数据库中一个单独的表中,也就是说每个实体 (entity) 都会被映射到数据库中,一个实体 (entity) 类对应数据库中的一个表。其中根实体 (root entity) 对应的表中定义了主键 (primary key),所有的子类对应的数据库表都要共同使用这个主键,同时这个表中和单表映射策略一样还定义了区分列 (DTYPE)。

    @Entity 
    @Inheritance(strategy=InheritanceType.JOINED) 
    @Table(name="Item_Joined") 
    public class Item implements Serializable { 
       private static final long serialVersionUID = 1L; 
       @Id 
       @GeneratedValue(strategy = GenerationType.AUTO) 
       private Long id; 
       private String title; 
       private Float price; 
       private String decription; 
        // Getters and Setters 
    }
    @Entity 
    public class Magazine extends Item { 
       private String isbn; 
       private String publisher; 
        // Getters and Setters 
    }
    @Entity 
    public class Phone extends Item { 
       private String factory; 
       private Float DurationTime; 
        // Getters and Setters 
    }
    

    对应ER图

    image.png

    这种映射策略和缺省的单表映射策略唯一的区别就是在根实体中使用 @Inheritance 注解 (annotation) 并指定使用 Joined 映射策略。在继承比较多时,查寻起来效率就会差一些,因为在查询的过程中需要多表的连接,连接的表数越多,查询效率越低下。DType关键字同单表策略

    Table-per-concrete-class

    这种映射策略和连接映射策略很类似,不同的是子类对应的表中要继承根实体 (root entity) 中的属性,根实体 (root entity) 对应的表中也不需要区分子类的列,表之间没有共享的表,也没有共享的列

    @Entity 
    @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) 
    @Table(name="Item_TPC") 
    public class Item implements Serializable { 
       private static final long serialVersionUID = 1L; 
       @Id 
       @GeneratedValue(strategy = GenerationType.AUTO) 
       private Long id; 
       private String title; 
       private Float price; 
       private String decription; 
        // Getters and Setters 
    }
    @Entity 
    public class Magazine extends Item { 
       private String isbn; 
       private String publisher; 
        // Getters and Setters 
    }
    @Entity 
    public class Phone extends Item { 
       private String factory; 
       private Float DurationTime; 
        // Getters and Setters 
     
    }
    
    

    对应的ER图

    image.png

    按上面配置的话,每个子表对应的根属性都是一样的,如果要修改数据库中的字段名称可以进行重写,如下例子

    @Entity 
    @AttributeOverrides({ 
       @AttributeOverride(name="id", 
                         column=@Column(name="Phone_id")), 
       @AttributeOverride(name="title", 
                         column=@Column(name="Phone_title")), 
       @AttributeOverride(name="description", 
                         column=@Column(name="Phone_desc")), 
    }) 
    public class Phone extends Item { 
     
       private String factory; 
       private Float DurationTime; 
     
        // Getters and Setters 
     
    }
    

    抽象实体类 (Abstract entity)

    上面的例子中所使用的 Item 是个具体类 (concrete class),并且使用 @Entity 注解 (annotation) 进行了注释。那么当 Item 类不是具体类 (concrete class),而是一个抽象类 (abstract class) 的时候,也就是当 Item 类的声明中使用了 abstract 关键字的时候,是如何影射的呢?事实上根实体是否是抽象实体类,在数据库中映射成的表没有任何区别。也就是说上面的例子中如果根实体类 Item 是个抽象实体类,使用了 abstract 关键字的话,在数据库中生成的表和上面的例子是相同的。唯一的区别就是,如果根实体是抽象实体类的话,就不能使用 new 关键字来生成这个实体类的对象了。他们的区别只是在 Java 语言语法上的区别,在持久化上没有任何区别。

    非实体类 (Nonentity)

    非实体类 (Nonentity) 也叫瞬态类 (transient class),就是普通的 POJO 类,没有使用 @Entity 注解 (annotation) 注释,这种类在持久化的时候不会被映射到数据库中,因为根据之前介绍过的持久化的原则,一个类如果想被持久化到数据库中,必须使用 @Entity 注解。

    个人感觉,这种好像实际用途不大。

    Mapped SupperClass

    JPA 有一种特殊的类叫做 Mapped Supper class,这种类不是实体类,他与实体类的区别就是用 @MappedSuperclass 注解来替代 @Entity 注解,其他方面没有变化。

    @MappedSuperclass 
    @Inheritance(strategy=InheritanceType.JOINED) 
    public class Employee { 
       @Id 
       @GeneratedValue 
       private Long id; 
       private String name; 
       private String depart; 
     
       // Getters and Setters 
     
    }
    

    这样在数据库中不用存在Employee对应的表。其他一切都可以正常使用

    我比较喜欢这个类型

    Repository接口

    继承JpaResposity的原理及基本用法

    JpaResposity这个接口是一个核心的接口,直接继承这个接口,就可以完成CURD的基本操作了。接口继承的路径如下图:

    Paste_Image.png

    最高层的Repository<T,ID>是一个空接口,我们定义的数据访问类只要实现这个接口,这个数据访问类就可以被spring data所管理,就此可以使用spring为我们提供操作方法(在原来的spring data中我们需要配置很多和Spring Data Repository相关的设置,但是现在有了spring boot,全部都已经自动配置好了)。这个接口要实现有两个泛型参数,第一个T表示实体类,第二个表示主键的类型,
    其中 JpaRepository 继承了2个接口 PagingAndSortingRepository 和QueryByExampleExecutor

    public interface StudentRepository extends JpaRepository<Student,Integer> {
    
        @Query("select s from Student s where s.id=?1")
        public Student loadById(int id);
    
        //根据地址和年龄进行查询
        public List<Student> findByAddressAndAge(String address, int age);
        //根据id获取对象,即可返回对象,也可以返回列表
        public Student readById(int id);
        //根据id获取列表,这里如果确定只有一个对象,也可以返回对象
        public List<Student> getById(int id);
        //根据id获取一个对象,同样也可以返回列表
        public Student findById(int id);
    }
    

    这个接口实现了JpaRepository接口,这里有两种使用JpaRepository的基本方法

    • 第一种方法增加了一个@Query的annotation,通过这个声明,Spring Data JPA就知道该使用什么HQL去查询数据,?1表示用方法中的第一个参数。
    • 第二种方法我们并没有定义任何的Annotation,在Spring Data JPA中提供了一种衍生查询,只要函数的声明有findBy,getBy,readBy即可,findByAddressAnAge表示根据address和age进行查询,注意大小写以及与对应Bean的属性对应,方法的第一个参数就是address,第二个参数就是age,这些方法的返回值可以是一个列表,也可以是一个对象,Spring Data JPA会自动根据返回类型来进行处理。我们不用写实现类,Spring Data JPA会自动帮助我们实现查询。

    Query 基本的使用方法

    JPQL 与 Hibernate的HQL类似

    • @Query 注解,标注在方法上,优先于 @NameQuery,也优先于在xml中定义的。
    • 最基本使用方法如下
     @Query("select u from User u where u.emailAddress = ?1")  
     User findByEmailAddress(String emailAddress); 
    
    • Like使用如下
     @Query("select u from User u where u.firstname like %?1")   
    List<User> findByFirstnameEndsWith(String firstname);
    
    • 原生的SQL
    @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)   
    User findByEmailAddress(String emailAddress);
    

    原生SQL目前不支持动态排序,如果使用分页的查询,可以考虑如下方式

    @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",    
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",    
    nativeQuery = true)   
    Page<User> findByLastname(String lastname, Pageable pageable); 
    
    • @Query中使用排序
    public interface UserRepository extends JpaRepository<User, Long> {
       @Query("select u from User u where u.lastname like ?1%")   
        List<User> findByAndSort(String lastname, Sort sort);
        @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")   
        List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
     }
    repo.findByAndSort("lannister", new Sort("firstname"));
    repo.findByAndSort("stark", new Sort("LENGTH(firstname)")); //这个是错误示例,不能这么用,需要参考下一条
    repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); 
    repo.findByAsArrayAndSort("bolton", new Sort("fn_len"));           
    
    
    • 使用参数名称
     @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")   
    User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname); 
    
    • Query 带修改
    @Modifying 
    @Query("update User u set u.firstname = ?1 where u.lastname = ?2") 
    int setFixedFirstnameFor(String firstname, String lastname);
    
    

    Jpa根据名称生成查询

    这个是spring-data-jpa最常见的部分了,不多解释,留一个备份在,被查即可。

    image.png image.png

    Repository为什么只有接口就可以直接使用

    这个问题我解释起来有些信心不足,不过可以肯定的是,我知道是什么,为什么还不是很清楚。
    在Spring-data-Jpa中有个默认的实现类org.springframework.data.jpa.repository.support.SimpleJpaRepository

    @Repository
    @Transactional(readOnly = true)
    public class SimpleJpaRepository<T, ID extends Serializable>
            implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
    //内容省略
    }
    

    EntityManager 是Jpa中操作数据库的类,在原生的Hibernate中叫做Session,在MyBatis中叫做SqlSession。
    Spring-data通过JpaRepositoryFactory将SimpleJpaRepository注入。所以我们不需要写实现类就可以了。

    Jpa自定义查询

    自定义查询,jpa已经帮我们完成了很多,只要我们自己写自己需要的条件就可以了,一般会写一个匿名类,重写toPredicate方法来完成。初步先记录下来。

    @Override
        public Page<Student> search(final Student student, PageInfo page) {
            return studentRepository.findAll(new Specification<Student>() {
                @Override
                public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                    
                    Predicate stuNameLike = null;
                    if(null != student && !StringUtils.isEmpty(student.getName())) {
                        stuNameLike = cb.like(root.<String> get("name"), "%" + student.getName() + "%");
                    }
                    
                    Predicate clazzNameLike = null;
                    if(null != student && null != student.getClazz() && !StringUtils.isEmpty(student.getClazz().getName())) {
                        clazzNameLike = cb.like(root.<String> get("clazz").<String> get("name"), "%" + student.getClazz().getName() + "%");
                    }
    
                    return cb.and(stuNameLike,clazzNameLike);
                }
            }, new PageRequest(page.getPage() - 1, page.getLimit(), new Sort(Direction.DESC, page.getSortName())));
        }
    

    再来一个例子,(xx or xx)and (ss or tt)的样子如何处理

    public void testSpecificaiton2() {
    //第一个Specification定义了两个or的组合
    Specification<Student> s1 = new Specification<Student>() {
        @Override
        public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
            Predicate p1 = criteriaBuilder.equal(root.get("id"),"2");
            Predicate p2 = criteriaBuilder.equal(root.get("id"),"3");
            return criteriaBuilder.or(p1,p2);
        }
    };
    //第二个Specification定义了两个or的组合
    Specification<Student> s2 = new Specification<Student>() {
        @Override
        public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
            Predicate p1 = criteriaBuilder.like(root.get("address"),"zt%");
            Predicate p2 = criteriaBuilder.like(root.get("name"),"foo%");
            return criteriaBuilder.or(p1,p2);
        }
    };
    //通过Specifications将两个Specification连接起来,第一个条件加where,第二个是and
    List<Student> stus = studentSpecificationRepository.findAll(Specifications.where(s1).and(s2));
    
        Assert.assertEquals(1,stus.size());
        Assert.assertEquals(3,stus.get(0).getId());
    }
    

    Jpa 分页和排序

    Spring-data-Jpa,帮我们实现了分页和排序,原则和上面的查询是一样的,我们可以在接口中定义,Spring-data-jpa会有一个默认的实现。

    分页

    分页主要有三个接口需要实现

    • PagingAndSortingRepository
    • Pageable
    • Page
      其中,如果XXRepository j继承自 JpaRepository,那么就已经继承了 PagingAndSortingRepository。 Pageable 是需要传入的参数的接口,它的实现类是PageRequest,PageRequest有3个构造方法,如下
    //这个构造出来的分页对象不具备排序功能
    public PageRequest(int page, int size) {
        this(page, size, (Sort)null);
    }
    //Direction和properties用来做排序操作
    public PageRequest(int page, int size, Direction direction, String... properties) {
        this(page, size, new Sort(direction, properties));
    }
    //自定义一个排序的操作
    public PageRequest(int page, int size, Sort sort) {
        super(page, size);
        this.sort = sort;
    }
    

    在自定义的Repository中如下定义就可以实现分页了

    Page<Student> findByAge(int age, Pageable pageable);
    

    排序

    Sort

    //可以输入多个Sort.Order对象,在进行多个值排序时有用
    public Sort(Sort.Order... orders)
    //和上面的方法一样,无非把多个参数换成了一个List
    public Sort(List<Sort.Order> orders)
    //当排序方向固定时,使用这个比较方便,第一个参数是排序方向,第二个开始就是排序的字段,还有一个方法第二个参数是list,原理相同
    public Sort(Sort.Direction direction, String... properties)
    

    Jpa 自定义Repository和创建自己的BaseRepository

    Jpa 自定义Repository

    主要遵循Jpa的规范,接口命名为 StudentRepositoryCustom,实现类命名为StudentRepositoryImpl。目前觉得意义不大,暂时不详细研究这一部分。

    创建自己的BaseRepository

    对于网上很多的文章来说,都要经过下面的几步

    • 创建BaseRepository接口 继承 JpaRepository
    • 创建 实现类,继承SimpleJpaRepository 并实现BaseRepository
    • 创建工厂,用来注入自己的实现类
    • 修改springboot的启动方法,或者修改配置文件,调用自己创建的工厂。
    @NoRepositoryBean
    public interface BaseRepository<T,ID extends Serializable> extends JpaRepository<T,ID> {
    }
    

    需要注意的是@NoRepositoryBean,这个表示该接口不会创建这个接口的实例(我们原来定义的StudentPageRepository这些,Spring Data JPA的基础组件都会自动为我们创建一个实例对象,加上这个annotation,spring data jpa的基础组件就不会再为我们创建它的实例)。

    但是,我目前个人感觉来说,不需要这么多步骤,我们只要实现第一步,再定义一些方法,基本可以由spring-data-jpa来实现就好了,再写一个基础的Service来实现一些通用的方法。

    参考资料

    http://blog.csdn.net/lyg_2012/article/details/70195062
    http://www.cnblogs.com/dreamroute/p/5173896.html
    https://www.ibm.com/developerworks/cn/java/j-lo-hibernatejpa/index.html
    https://www.ibm.com/developerworks/cn/java/j-lo-jpaprimarykey/
    http://blog.csdn.net/xiao_xuwen/article/details/53420835
    http://blog.csdn.net/xiao_xuwen/article/details/53579353

    相关文章

      网友评论

        本文标题:SpringBoot-2-Jpa

        本文链接:https://www.haomeiwen.com/subject/otifgttx.html