【SpringBoot框架篇】11.Spring Data Jpa实战

简介

首先了解一下 JPA,Spring Data Jpa,之间的关系

JPA

JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。Sun引入新的JPA ORM规范出于两个原因:其一,简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下归一。
JPA包括以下3方面的技术:

  • ORM映射元数据: 支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系
  • API: 操作实体对象来执行CRUD操作
  • 查询语言: 通过面向对象而非面向数据库的查询语言(JPQL)查询数据,避免程序的SQL语句紧密耦合

Spring Data Jpa

下面引入官网的介绍
在这里插入图片描述

Hibernate

Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的JaveEE架构中取代CMP,完成数据持久化的重任

Jpa、Spring Data Jpa、Hibernate三者之间的关系

Hibernate是Spring Data Jpa得默认实现方式
在这里插入图片描述

引入依赖

        <!--spring data jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--mysql依赖-->    
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

配置文件

server:
  port: 8011

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/jpa_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=GMT%2b8&characterEncoding=utf8&connectTimeout=10000&socketTimeout=3000&autoReconnect=true&rewriteBatchedStatements=true
    username: root
    password: 123456
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      #自动创建或修改表结构
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    properties:
      hibernate:
        #设置hibernate方言使用mysql的InnoDBD引擎,InnoDBD支持事务
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
        #启用懒加载
        enable_lazy_load_no_trans: true

实体类常用注解

@Entity和@Table

@Entity
@Table(name = "sys_user")
public class User{
    
    }  
  • @Entity 表明该类 (User) 为一个实体类,它默认对应数据库中的表名是user。这里也可以写成
    @Entity(name = “sys_user”)
    或者
    @Entity
    @Table(name = “user”)
  • @Table 当实体类与其映射的数据库表名不同名时需要使用 @Table注解说明,该标注与 @Entity 注解并列使用

@Index

@Index注解用来添加表的索引,可以指定单个字段和多个字段,多个字段之间用 ’ , ’ 隔开

@Entity
@Table(name = "sys_user",indexes = {
    
    
        @Index(name = "username",  columnList = "username"),
        @Index(name = "address_age",  columnList = "address,age")
}
public class User{
    
    
	  private String username;
	  private String address;
	  private Integer age;
}

@Id和@GeneratedValue

  • @Id 标识此字段是主键
  • @GeneratedValue 设置主键的自增策略,默认是AUTO
    • TABLE 使用一个特定的数据库表格来保存主键。
    • SEQUENCE 根据底层数据库的序列来生成主键,条件是数据库支持序列。
    • IDENTITY 主键由数据库自动生成(主要是自动增长型)
    • AUTO 主键由程序控制。
  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private Integer id;

@Column

@Column 注释定义了将成员属性映射到关系表中的哪一列和该列的结构信息,属性如下:

属性名 描述
name 映射的列名。如:映射tbl_user表的name列,可以在name属性的上面或getName方法上面加入;
unique 是否唯一;
nullable 是否允许为空;
length 对于字符型列,length属性指定列的最大字符长度;
insertable 是否允许插入;
updatable 是否允许更新;
    @Column(name = "username", unique = true, nullable = false,updatable = false,length = 36)
    private String username;

@Transient

@Transien就是在给某个javabean上需要添加个属性,但是这个属性你又不希望给存到数据库中去,仅仅是做个临时变量,用一下。不修改已经存在数据库的数据的数据结构。

    @Transient
    private String ignoreColumn;

继承超类的通用字段属性

@Inheritance(strategy= InheritanceType.TABLE_PER_CLASS)//选择继承策略
@MappedSuperclass
public class BaseModel implements Serializable {
    
    
	
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
	private Date createdDate;
}
@Entity
@Table(name = "sys_user")
public class User extends  BaseModel{
    
    
	//这里就不用再写通用的字段了。
}  

@MappedSuperclass

  • 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
  • .标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。
    我们可以把一些通用的表字段放到超类中定义

@Inheritance

Hibernate继承映射在 Annotation 中使用 @Inheritance 注解,并且需要使用 strategy 属性指定继承策略,继承策略有 SINGLE_TABLE、TABLE_PER_CLASS 和 JOINED 三种。

  • SINGLE_TABLE 是将父类和其所有的子类集合在一块,存在一张表中,并创建一个新的字段来判断对象的类型。
  • TABLE_PER_CLASS 是为每一个类创建一个表,这些表是相互独立的。
  • JOINED 是将父类、子类分别存放在不同的表中,并且建立相应的外键,以确定相互之间的关系

审计功能

我们一般对记录的创建和修改需要手动set操作时间
user.setCreateDate(new Date());
但是通过使用Jpa的审计功能,就可以交给Jpa去实现了.

启用审计功能

在启动类里添加注解

@EnableJpaAuditing
public class SpringDatajpaApplication {
    
    }

在需要使用的类上面添加审计监听器
在需要使用审计功能的类上面加@EntityListeners注解指定监听类为AuditingEntityListener
一般我们会这些审计的通用字段放到超类里面

@EntityListeners(AuditingEntityListener.class)
public class BaseModel {
    
    }

审计功能注解

  • @CreatedDate 在记录创建的时候自动插入创建时间
  • @CreatedBy 在记录创建的时候自动插入创建者名称
  • @LastModifiedDate 在记录修改的时候自动修改操作 时间
  • @LastModifiedBy 在记录修改的时候自动修改修改者名称

添加审计人

我这里写死了是admin,实际使用场景应该获取当前登录人的用户名

@Component
public class AuditorAwareImpl implements AuditorAware<String> {
    
    
    @Override
    public Optional<String> getCurrentAuditor() {
    
    
        return Optional.of("admin");
    }
}
@Inheritance(strategy= InheritanceType.TABLE_PER_CLASS)//选择继承策略
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseModel implements Serializable {
    
    
    @CreatedDate
    @Column(name = "created_date", updatable = false)
    private Date createdDate;

    @CreatedBy
    @Column(name = "created_by", updatable = false, length = 64)
    private String createdBy;

    @LastModifiedDate
    @Column(name = "updated_date")
    private Date updatedDate;

    @LastModifiedBy
    @Column(name = "updated_by", length = 64)
    private String updatedBy;
}
@Entity
@Table(name = "sys_user")
@Data
public class User  extends  BaseMode{
}

下面我插入一条数据,能看到审计字段也都自动插入了.
在这里插入图片描述
在这里插入图片描述

复合主键

在我们的日常开发中,有时候会用到数据库进行设计的时候,采用了复合主键来来保证唯一性,
hibernate的 @EmbeddedId 嵌入式主键应用于实体类或映射超类的持久字段或属性,以表示可嵌入类的组合主键。 可嵌入的类必须标注为 @Embeddable 下面介绍一下采用
下面以用户角色关联表介绍.

@Entity
@Table(name = "user_role")
@Data
public class UserRole {
    
    

    public UserRole(){
    
     }

    public UserRole(Integer userId, Integer roleId){
    
    
    this.id=new PK(userId,roleId);
    }
    
    @EmbeddedId
    private PK id;
    
    /**
     * 联合主键
     */
    @Embeddable
    @Data
    public static class PK implements Serializable {
    
    

        public PK() {
    
     }

        public PK(int userId, Integer roleId) {
    
    
            this.userId = userId;
            this.roleId = roleId;
        }
        /**
         * 用户id
         */
        private Integer userId;

        /**
         * 角色id
         */
        private Integer roleId;
  }
}

Spring Data Jpa 接口使用详解

创建一个接口,然后继承JpaRepository<T,ID>,T是表对应的实体类,ID是主键的类型

public interface UserRepository extends JpaRepository<User,Integer> {
    
    }

JpaRepository默认提供的方法

我这边列出了一些常用的方法
@Autowired
UserRepository userRepository;
userRepository.findAll();
下面的方法可以直接通过userRepository直接调用

	//查询所有信息
 	List<T> findAll();
 	//保存并且刷新
    <S extends T> S saveAndFlush(S var1);
  	//根据主键删除
    void deleteById(ID var1);
    //根据实体对象删除
    void delete(T var1);
    //根据主键获取对象信息
    T getOne(ID var1);
    //添加或则修改
	<S extends T> S save(S var1);

查询创建Query Creation

Spring Data Jpa通过解析方法名创建查询,框架在进行方法名解析时,会先把方法名多余的前缀find…By, read…By, query…By, count…By以及get…By截取掉,然后对剩下部分进行解析,第一个By会被用作分隔符来指示实际查询条件的开始。 我们可以在实体属性上定义条件,并将它们与And和Or连接起来,从而创建大量查询:

关键字 示例 JPQL
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age ⇐ ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ? (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ? (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection age) … where x.age not in ?1
TRUE findByActiveTrue() … where x.active = true
FALSE findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

自定义查询@Query

位置参数绑定

下标重1开始

		@Query("select u from User u where u.username = ?1 and u.password = ?2")
		User getByUsernameAndPassword(String username, String password);

命名参数绑定

		@Query("select u from User u where u.username = ?1 and u.password = ?2")
		User getByUsernameAndPassword(String username, String password);

命名参数绑定

		@Query("select u from User u where u.username = :username and u.password = :password")
		User getByUsernameAndPassword(@Param("username")String username,@Param("password") String password);

原生查询Native Queries

需要在@Query注解中加nativeQuery = true
注意,from后面对应的是表名称,而不是实体类的名称

	    @Query(value = "select u.* from sys_user u where u.username = :username and u.password = :password",nativeQuery = true)
	    User getByUsernameAndPasswordSQl(@Param("username")String username,@Param("password") String password);

删除修改操作

单独使用@Query注解只是查询,如涉及到修改、删除则需要再加上@Modifying和@Transactional注解

	    @Transactional
	    @Modifying
	    @Query("delete from User  where id=?1")
	    void deleteByUserId(Integer id);

分页查询

		Page<User> findAllByUsernameLike(String username, Pageable pageable);

构建Pageable 对象的方法

参数 描述
page 页码,重0开始
size 每页显示的数量
direction 排序类型, Sort.Direction.DESC是降序,Sort.Direction.ASC是升序
properties 排序的字段,可变参数。
		//2.0版本之前
		new PageRequest(int page, int size);
		new PageRequest(int page, int size, Direction direction,String... properties);
		//2.0版本之后        
		PageRequest.of(int page, int size);
		PageRequest.of(int page, int size, Direction direction, String... properties);

完整代码

        Pageable pageable = PageRequest.of(0, 10, Sort.Direction.ASC, "id");
        Page<User> userPage = userRepository.findAllByUsernameLike("user%", pageable);
        System.out.println("总记录数"+userPage.getTotalElements()+",总页数"+userPage.getTotalPages());
        for (User user : userPage.getContent()) {
    
    
            System.out.println(user.getUsername());
        }

使用Specification进行动态参数查询

对于查询条件不固定的情况下,JPQL实现不了这种情况,这个时候要继承JpaSpecificationExecutor
接口来实现动态参数查询了.
JpaSpecificationExecutor接口主要的2个方法

	    List<T> findAll(@Nullable Specification<T> var1);
	    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);

首先继承JpaSpecificationExecutor接口

public interface UserRepository extends JpaRepository<User,Integer>, JpaSpecificationExecutor {
    
    }

然后组装Specification对象

 		String startDate = "2020-05-22";
        String endDate = "2020-05-25";
        String username = "dominick";
        Specification<User> querySpecifi = new Specification<User>() {
    
    
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
    
    
                List<Predicate> predicates = new ArrayList<>();
                if (!StringUtils.isEmpty(startDate)) {
    
    
                    //大于或等于传入时间
                    predicates.add(cb.greaterThanOrEqualTo(root.get("createDate").as(String.class), startDate));
                }
                if (!StringUtils.isEmpty(endDate)) {
    
    
                    //小于或等于传入时间
                    predicates.add(cb.lessThanOrEqualTo(root.get("endDate").as(String.class), endDate)));
                }
                if (!StringUtils.isEmpty(username)) {
    
    
                    //模糊查询,需要自己手动拼接%%字符
                    predicates.add(cb.like(root.get("username").as(String.class), "%" + username + "%"));
                }
                // and到一起的话所有条件就是且关系,or就是或关系
                return cb.and(predicates.toArray(new Predicate[predicates.size()]));
            }
        };
        List<User> userList = userRepository.findAll(querySpecifi);

复杂的原生sql查询

通过EntityManagerFactory构建原生sql查询
下面只是简单的示范了怎么用,然后可以根据需求写对应的sql,能用sql解决的都不是难事。。

public interface UserCustomRepository {
    
    
    List<Object[]> findBynativeQuery(User user);
}

@Component
public class UserCustomImplRepsotory implements UserCustomRepository {
    
    

    private EntityManagerFactory emf;

    @PersistenceUnit
    public void setEntityManagerFactory(EntityManagerFactory emf) {
    
    
        this.emf = emf;
    }

    @Override
    public List<Object[]> findBynativeQuery(User user) {
    
    
        EntityManager em = emf.createEntityManager();
        try {
    
    
            Query query = null;
            StringBuilder sql = new StringBuilder("select * from sys_user  where 1=1 ");
            List<Object> condition = new ArrayList<>();
            int index = 0;
            if (!StringUtils.isEmpty(user.getUsername())) {
    
    
                index++;
                condition.add(user.getUsername());
                sql.append(" and username=?" + index);
            }
            if (!StringUtils.isEmpty(user.getChannelId())) {
    
    
                index++;
                condition.add(user.getChannelId());
                sql.append(" and channelId=?" + index);
            }
            //创建query对象
            query = em.createNativeQuery(sql.toString());
            //注入参数
            if (index != 0) {
    
    
                for (int i = 1; i <= index; i++) {
    
    
                    //query的parameter 下标起始位置重1开始的
                    query.setParameter(i, condition.get(i - 1));
                }
            }
            List<Object[]> success = query.getResultList();
            em.close();
            return success;
        } finally {
    
    
            if (em != null) {
    
    
                em.close();
            }
        }
    }
}

调用代码

		@Autowired
	    UserCustomImplRepsotory userCustomImplRepsotory;
   
  		User user=new User();
        user.setUsername("dominick_li");
        user.setChannelId(1);
        List<Object[]>  list=userCustomImplRepsotory.findBynativeQuery(user);
        for(Object[] objs:list){
    
    
            System.out.println(objs[0]+","+objs[1]);
        }

级联加载信息

下面级联查询我都用了懒加载
fetch = FetchType.LAZY
需要在配置文件把懒加载的开关打开

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true #开启懒加载

一对一

使用@OneToOne注解实现一对一的关系映射
@JoinColumn中的name标识当前实体的外键字段,referencedColumnName标识用户表roleId和Role表的关联字段。
我这里用 用户表和角色表介绍,省略了其它字段和注解,只描述重点

public class User {
    
    
	private Integer roleId;
	@OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="roleId",referencedColumnName="id",insertable = false, updatable = false)
    private Role role;
}
public class Role(
	private Integer id;
	private String roleName;
)

用户表数据
在这里插入图片描述
角色表数据
在这里插入图片描述
测试结果

       User user=userRepository.getOne(id);;
       System.out.println("角色名称是:"+user.getRole().getRoleName());

在这里插入图片描述

一对多

一对多使用 @OneToMany 注解实现一对一关系映射
@JoinColumn name属性指的是多的一方外键,也就是用户表的channelId属性,referencedColumnName表示当前类和用户类关联的列,默认是主键
我这里使用渠道表和用户表介绍

public class User {
    
    
	private Integer channelId;
}
public class Channel {
    
    
	
    private Integer id;
    private String channelName;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name="channelId")
    private List<User> users;
}

渠道表数据
在这里插入图片描述
用户表数据
在这里插入图片描述
测试级联查询

        Channel channel = channelRepository.getOne(id);
        System.out.println(channel.getChannelName() + "有" + channel.getUsers().size() + "个用户");

在这里插入图片描述

多对一

多对一使用 @ManyToOne 注解进行关系映射
@JoinColumn name属性指的是多的一方外键,也就是user表的channelId属性,referencedColumnName表示渠道表和用户关联的列,默认是主键
我这里使用用户表和渠道表作介绍

public class User{
    
    
	 private Integer id;
	 private Integer channelId;
	  @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="channelId",referencedColumnName="id",insertable = false, updatable = false)
    private Channel channel;
}

private class Channel{
    
    
	private Intger id;
	private String channelName;
}

用户表数据
在这里插入图片描述
渠道表数据
在这里插入图片描述
测试级联加载结果

        User user=userRepository.getOne();
        System.out.println("渠道名称是:"+user.getChannel().getChannelName());

在这里插入图片描述

多对多

多对多关系用 @ManyToMany 注解进行关系映射
还需要依赖@JoinTable注解关联 关系表

public  class User{
    
    
	private int id;
	@ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
            name = "user_role", joinColumns = {
    
    @JoinColumn(name = "userId")},
            inverseJoinColumns = {
    
    @JoinColumn(name = "roleId")})
    private List<Role> roles;
}
public class Role{
    
    
	private int id;
}

public class UserRole{
    
    
	private Intger userId;
	private Intger roleId;
}

用户表
在这里插入图片描述
角色表
在这里插入图片描述
用户角色关系表
在这里插入图片描述
测试级联加载结果

       User user=userRepository.getOne();
       System.out.println("用户拥有:"+user.getRoles().size()+"个角色");

在这里插入图片描述

开启jpa SaveAll批量新增或修改操作

jpa默认的saveAll是在里面通关while不停的调用 save方法,假设要添加500条数据,则需要
执行500条insert语句才能完成操作,sql执行时间会长,jpa提供了批量执行sql的功能,下面
我来带大家开启批处理功能
在这里插入图片描述

配置文件

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        #检测批处理开关是否打开
        generate_statistics: true
属性 描述
batch_size 设置批处理一次处理多少条数据,如果大小设置为50,假设执行saveAll的数据300条,则需要执行6次sql才能处理完请求。
generate_statistics 是否检测批处理开关是否打开

测试代码

注意: 如果设置主键为自增类型,则批处理还是会失效。如果想使用批处理,尽量用雪花算法生成ID或者UUID

Model

@Entity
@Table(name = "orders")
@Data
public class Orders  {
    
    
    @Id
    private String id;
    private String orderName;
}

Dao层

public interface OrdersRepository extends JpaRepository<Orders,String> {
    
    
}

web层

@RestController
public class OrdersController {
    
    
    @Autowired
    OrdersRepository ordersRepository;
    @RequestMapping("/saveAll")
    public String saveAll(){
    
    
        Orders orders=null;
        List<Orders> ordersList=new ArrayList<>(100);
        for(int i=0;i<300;i++){
    
    
            orders=new Orders();
            //订单表的主键应该使用雪花算法之类生成,雪花算法生成的id是有序的,这样索引查询起来会快很多,uuid是无序的,查询效率极低。。。。
            orders.setId(UUID.randomUUID().toString());
            orders.setOrderName("订单_"+1);
            ordersList.add(orders);
        }
        ordersRepository.saveAll(ordersList);
        return "success";
    }

}

没开启的批处理

SQL Stat View JSON API 是druid连接池的一个监控工具,我会在下一章博客教大家使用
可以看到下面执行数是300
在这里插入图片描述

开启批处理后

可以看到sql执行数是6,和我们预期的一致。
在这里插入图片描述
下面是控制打印的
在这里插入图片描述

项目配套代码

github地址
要是觉得我写的对你有点帮助的话,麻烦在github上帮我点 Star

【SpringBoot框架篇】其它文章如下,后续会继续更新。

猜你喜欢

转载自blog.csdn.net/ming19951224/article/details/106308544