Spring Data JPA

简介

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,可使开发者用极简的代码即可实现对数据库的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用 Spring Data JPA 可以极大提高开发效率!

Spring Data JPA 让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现,在实际的工作工程中,推荐使用Spring Data JPA + ORM(如:hibernate)完成操作,这样在切换不同的ORM框架时提供了极大的方便,同时也使数据库层操作更加简单,方便解耦

自定义对象接收

在实际工作中,我们经常遇到的场景是查询数据表中的某几个字段,但是使用 Spring Data Jpa框架一般查询结果只返回对应的Entity实体。我们是无法直接操作 Spring Data JPA提供的一些方法获得自定义实体类。

例如。我们如果想拿用户表中的姓名和生日字段,我们可能写出如下的操作。

// entity 实体类
public class User {
     private Stirng name;
    private String birthday;
    /**
    * 其他字段
    */
}
public class UserDTO  {
    private Stirng name;
    private String birthday;
    /**
    * 省略get set方法
    */
}

// dao层
public interface UserDao extends JpaSpecificationExecutor<User>,
        JpaRepository<User, Integer> {
     @Query("select name, birthday from user where name = ?1", nativeQuery=true)    
     List<UserDTO> findByName(String name);
}

实际测试可以测到返回的是Object[]类型,并非我们想要得到的UserDTO对象。那么我们应该如何操作呢

当然,我们也可以这样操作,用Object[]接收,然后对数组进行解析。

//转换实体类
    public static <T> List<T> castEntity(List<Object[]> list, Class<T> clazz) throws Exception {
        List<T> returnList = new ArrayList<T>();
        if(CollectionUtils.isEmpty(list)){
            return returnList;
        }
        Object[] co = list.get(0);
        Class[] c2 = new Class[co.length];
        //确定构造方法
        for (int i = 0; i < co.length; i++) {
            if(co[i]!=null){
                c2[i] = co[i].getClass();
            }else {
                c2[i]=String.class;
            }
        }
        for (Object[] o : list) {
            Constructor<T> constructor = clazz.getConstructor(c2);
            returnList.add(constructor.newInstance(o));
        }
        return returnList;
    }

实体类

@Entity
@Table(name = "user")
@Data
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // 姓名
    @Column(name = "name")
    private String name;

    // 昵称
    @Column(name = "nickname")
    private String nickname;

    // 出生日期
    @Column(name = "birthday")
    private LocalDate birthday;

    // 性别
    @Column(name = "sex")
    private Boolean sex;

    //密码
    @Column(name = "password")
    private String password;
}

我们想要查询数据表中的某几个字段,有以下三种方法。

Spring Data Jpa支持的关键字搜索

  1. 自定义一个返回对象
@Data
@ToString
//@NoArgsConstructor 不可写,否则会出现类型转换错误
@AllArgsConstructor // 必须写,转换时会调用
public class UserDTO implements Serializable {

    private String name;

    private String nickname;

    private LocalDate birthday;

}
  1. Dao层定义 repository类
  2. 注意
  • 这种方法存在局限性,只能使用JPA提供的一些方法。如果业务复杂,或者查询条件为空等,则无法处理
public interface UserDao extends JpaSpecificationExecutor<User>,
        JpaRepository<User, Integer> {
    /**
     * 根据id查询
     * @param id
     * @param type
     * @param <T>
     * @return
     */
    <T> Optional<T> findById(int id, Class<T> type);
}
  1. 测试
  2. 测试
    @Autowired
    private UserDao dao;
@Test
public void findByIdTest() {
    Optional<UserDTO> userDTO = dao.findById(1, UserDTO.class);
    if (userDTO.isPresent()) {
        System.out.println(userDTO);
    }
}

自定义一个返回对象

需求:查询用户姓名,昵称,出生日期、

  1. 自定义一个返回对象
@Data
@ToString
//@NoArgsConstructor 不可写,否则会出现类型转换错误
@AllArgsConstructor // 必须写,转换时会调用
public class UserDTO implements Serializable {

    private String name;

    private String nickname;

    private LocalDate birthday;

}
  1. Dao层定义 repository类 ,使用@Query注解,JPQL
  1. 注意:
  • 当返回对象为实体类中的某几个属性时,字段名和实体类必须保持一致。否则无法映射
  • 如果实体类中的属性名为驼峰例如:userName,映射到数据库则为user_name
  • 使用全限定类名com.kxj.jpa.dto.UserDTO,必须有new关键字
public interface UserDao extends JpaSpecificationExecutor<User>,JpaRepository<User, Integer> {
   /**
 * 根据用户名查找
 * @param name
 * @return
 */
@Query(value = "select new com.kxj.jpa.dto.UserDTO(u.name, u.nickname, u.birthday) from User u where u.name = :name")
    List<UserDTO> findByUserName(@Param("name") String name);
}
  1. 测试
  2. 测试
@Test
public void findByUserNamesTest() {
    List<UserDTO> userDTOS = dao.findByUserName("tom");
    for (UserDTO userDTO : userDTOS) {
        System.out.println(userDTO);
    }
}

自定义接口

  1. 自定义一个接口
    public interface UserDTO2 {
    String getName();
/**
     * 当别名与该getXXX名称不一致时,可以使用该注解调整
     * @return
     */
    @Value("#{target.nickName}")
    String getNickname();

    LocalDate getBirthday();

    // 主要起打印作用
    default String toStringInfo() {
        return getName() + " " + getNickname() + " " + getBirthday();
    }
}

Dao层定义 repository类 ,使用@Query注解,JPQL

注意:

  • 当返回对象为接口时,SQL语句中查询的字段为 类.字段 ,eg: u.name . 则必须写别名,和接口中getXXX保持一致,否则无法转换,为null。
  • 如果别名与该getXXX名称不一致时,也可在接口中使用@Value("#{target.XXX}")和SQL中别名保持一致
public interface UserDao extends JpaSpecificationExecutor<User>,
         JpaRepository<User, Integer> {
   /**
      * 根据用户名查找
      * 别名必须写 在HQL方式中 否则无法转换 为null,在原生SQL中别名可写可不写
      * @param name
      * @return
      */
 @Query(value = "select u.name as name, u.nickname as nickName, u.birthday as birthday from User u where u.name = :name")
     List<UserDTO2> findByUserName(@Param("name") String name);
 }
  1. 测试
@Test
public void findByNameTest() {
    List<UserDTO2> list = dao.findByUserName("tom");
    list.forEach(user -> System.out.println(user.toStringInfo()));
}
  1. 如果我们想获取Page对象,则查询的时候加上Pageable对象
    注意
  • 传入Pageable对象,JPA会自动帮我们解析。查询的时候回多出一条查询条数的SQL
/**
   * 分页 根据用户名查找
   * @param name
   * @param pageable
   * @return
   */

@Query(value = "select u.name as name, u.nickname as nickName, u.birthday as birthday from User u where u.name = :name") Page<UserDTO2> findByUserName(@Param("name") String name, Pageable pageable);

  1. 测试
    注意
  • 当数据表中的查询总条数大于传入的size(每页显示的条数时,才会触发查询总条数
@Test
 public void findByNamePageTest() {   
    Pageable pageable = PageRequest.of(0, 1);
  Page<UserDTO2> page = dao.findByUserName("tom", pageable);
  page.getContent().forEach(user -> System.out.println(user.toStringInfo()));
  System.out.println("条数:" + page.getTotalElements());
}

通过配置输出SQL,可以在控制台看到输出的SQL语句是两条

6. 使用原生SQL

我们除了使用JPQL查询外,也可以使用原生SQL进行查询

/**
        * 分页 根据用户名查找 原生SQL
        * @param name
        * @param pageable
        * @return
        */
   @Query(value = "select name, nickName, birthday from user  where name = :name", nativeQuery = true)
   Page<UserDTO2> findByUserNameUseNativeSQL(@Param("name") String name, Pageable pageable);
  1. 测试同上例

处理实体类中未有的属性

需求:查询用户重名的个数及姓名

  • 自定义对象
  1. 用法大致和上述无区别,知识多加属性和查询的字段保持一致
  • 注意
  1. 注意:count()查询返回的数据类型是Long类型,如果用其他类型接收,会报错
@Data
@ToString
@AllArgsConstructor // 必须写,转换时会调用
public class UserDTO3 {

    private String name;

    /**
     * 数量
     */
    private Long count;
}
/**
     * 查找名字相同的数量 类
     * @return
     */
@Query(value = "select new com.kxj.jpa.dto.UserDTO3(u.name, count(u.id)) from User u group by u.name")
List<UserDTO3> findUserCount();
  • 自定义接口
public interface UserDTO4 {

    String getName();

    Long getCount();

    default String toStringInfo() {
        return getName() + " " + getCount();
    }
}
/**
     * 查找名字相同的数量 接口
     * @return
     */
@Query(value = "select name, count(id) as count from user group by name", nativeQuery = true)
List<UserDTO4> findUserNameCount();

上面也可以使用原生SQL查询

以上测试省略

总结

  1. 采用自定义对象接收时,可以分场景使用不同方式
  2. 自定义对象和自定义接口的区别,例如自定义对象,属性要和实体类保持一致,要有全参构造等
  3. 如果遇到聚集函数等实体类没有定义的属性应该添加字段相对应的属性到DTO类中