JPA使用SQL的一些总结

一. 概述

简单的增删改查使用 JPA 非常方便,复杂的 Where 条件我们也在 Specification 和 CriteriaBuilder 的基础上封装了 WhereBuilder,实现了类似 C# 的 LINQ 的写法,但是在很多情况下是需要使用 SQL 的,而且很多情况下原生 SQL 效率高很多。
JPA 支持二种最基本的 SQL 方式,Native SQL 和 JPQL,JPQL 语法非常类似于SQL语法,但是是针对 Java 对象和实体。我更倾向原生 SQL,毕竟 SQL 是通用的标准,开发人员或多或少都是掌握 SQL 的。
以下是在 JPA 里使用 SQL 的一些总结,不同的情况下有不同的应对方式。

二. @Query注解的方式

我们可以直接在 DAO 的接口里定义 SQL,以下列出了7种方式:

@Repository
public interface UserDao extends JpaRepository<UserEntity, String>, JpaSpecificationExecutor<UserEntity> {
    @Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
    Optional<UserEntity> test1(String name);

    @Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
    Optional<UserVm> test2(String name);

    @Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
    Optional<IUserVm> test3(String name);

    @Query("select new d1.dxdevices.iot.user.model.UserVm( u.name,u.id,u.remark) from UserEntity u where u.name like %?1%")
    Optional<UserVm> test4(String name);

    @Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
    Optional<Object> test5(String name);

    @Query(nativeQuery = true, value = "select * from d1_user where callback_url is null")
    Page<UserEntity> test6(Pageable pageable);

    @Query(nativeQuery = true, value = "select * from d1_user where callback_url is null",
            countQuery = "select count(*) from d1_user where callback_url is null")
    Page<UserEntity> test7(Pageable pageable);
}

1. test1

@Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
Optional<UserEntity> test1(String name);

使用原生 SQL 一定需要加上 nativeQuery = true, 动态参数使用 ?1这种问号加数字的方式,比较特殊的是字段和表名一定得用上数据库真实的值,比如 UserEntity 对应的表名是 d1_user,原生的 SQL 是不可以 select * from UserEntity
还有一个就是驼峰的双词字段,需要换成下划线的方式,比如

/**
  * API回调地址
*/
@Column(length = 255)
@Comment("API回调地址")
private String callbackUrl;

UserEntity 下的字段 callbackUrl 映射到表里面字段名就变成 callback_url了,原生的 SQL 是不可以 select callbackUrl from d1_user,而应该是select callback_url from d1_user,总之原生 SQL 是可以直接拷贝到 Navicat 等 SQL 工具里直接执行成功的。

2. test2

@Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
Optional<UserVm> test2(String name);

和 test1 的差别是test1返回的就是 UserEntity ,但是 test2 返回的是 UserVm,是我们自己定义的一个实体对象,它的字段和 UserEntity 部分字段一样。这样的情况很常见,我们需要查询出数据转换成我们定义的实体对象,而不是数据库对应的 Entity。
实际上这个 test2 是执行失败的,这样是不行的,会报 Converter 错误,因为我们没有定义映射关系,即使 UserVm 和 UserEntity 字段完全一样也不行。

3. test3

@Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
Optional<IUserVm> test3(String name);

为了解决 test2 不行的问题,我们对应有几种解决方式,最简单的就是把实体类换成 interface 就可以了。

public interface IUserVm {
    String getId();
    LocalDateTime getCreateTime();
    String getName();
    String getCallbackUrl();
    String getCallback_Url();
}

但是注意因为是原生 SQL ,IUserVm 里的 get 方法也必须和表里面的字段保持一致,参考上面,其中 getCallbackUrl()不可以,而 getCallback_Url() 才能获取到值

4. test4

@Query("select new d1.dxdevices.iot.user.model.UserVm( u.name,u.id,u.remark) from UserEntity u where u.name like %?1%")
Optional<UserVm> test4(String name);

为了解决 test2 不行的问题,还有一种办法,就是不用原生 SQL,使用 JPQL ,可以直接在 select 里面加上自定义实体类的构造函数,注意必须把实体类的完整 package 写全。这个时候表名和字段名又不能按数据库表的来了,得按 Entity 对象定义的来了。

5. test5

@Query(nativeQuery = true, value = "select * from d1_user where name like %?1%")
Optional<Object> test5(String name);

更通用的方式,或者说最原始的方式,类似我们早期用 JDBC 的方式,返回的是 ResultSet,是一个 Object 的数组,然后由调用者自己去做映射,以下是调用 test5 的代码

public void test5(){
  Optional<Object> optionalUserEntity = userDao.test5("ad");
  if (optionalUserEntity.isEmpty()) {
      return;
  }
  Object obj = optionalUserEntity.get();
  UserVm userVm = new UserVm(obj);
  System.out.println(userVm.getName());
}

返回的 Object 其实是一个 Object[] 数组,我们可以给 UserVm里增加一个构造函数,参数是这个数组。

public UserVm(Object obj) {
  if (obj instanceof Object[] objs) {
      this.name = objs[0].toString();
      //......
  }
}

这里要注意的是,这个数组里对应的元素的顺序是按表里定义字段的顺序,一点儿也不能错

6. test6

@Query(nativeQuery = true, value = "select * from d1_user where callback_url is null")
Page<UserEntity> test6(Pageable pageable);

原生的 SQL 要支持分页可以通过在 sql 语句里面加 limit offset 之类的关键字,但是更简单的方式是使用 Pageable ,这个我们也一直在使用,也就是加上 Pageabele 参数后,JPA 会在 sql 后面自动加上 limit 和 offset,我相信切换一个其他类型的数据库, JPA 会自动拼接特定的关键字。
另外为了返回 Page 类型的结构体,需要查询总数,JPA 会根据你的 sql 自动拼接出查询总数的 sql 语句。

Hibernate: /* dynamic native SQL query */ select * from d1_user where callback_url is null limit ? offset ?
Hibernate: /* dynamic native SQL query */ select count(*) from d1_user

7. test7

@Query(nativeQuery = true, value = "select * from d1_user where callback_url is null",countQuery = "select count(*) from d1_user where callback_url is null")
Page<UserEntity> test7(Pageable pageable);

相比 test6 ,test7 增加了一个 countQuery,在当前的例子里,这个写不写都没有问题,但是在一些特殊的情况下,你的 sql 比较复杂的时候,有可能 JPA 无法准确正确的自动拼接出查询总数的 sql 语句,所以 JPA 提供了这个属性来让开发者自己定义查询总数的 sql。

8. 动态 sql

举一个例子,我们根据用户来查询这个用户名下的所有设备,假如传递过来的用户是admin,则返回所有设备,否则就返回这个用户下的设备。如果用原生 SQL 的方式则比较麻烦了。

@Query(nativeQuery = true, value = "select * from d1_device where name=?1")
Page<DeviceEntity> test8(String name,Pageable pageable);

如果按照以上的写法是无法实现这个需求的。JPA 提供了一个 QueryRewriter 来实现这个功能,可惜是 JPA 3.0 才有的功能,而我们现在用的是 JPA 2.6,所以我没有写相关的例子,基本用法也很简单,类似:

@Query(value = "select original_user_alias.* from SD_USER original_user_alias",nativeQuery = true, queryRewriter = MyQueryRewriter.class)
List<User> findByNativeQuery(String param);

然后定义一个 MyQueryRewriter

public class MyQueryRewriter implements QueryRewriter {
    @Override
    public String rewrite(String query, Sort sort) {
        if(query.contain("admin"){//在执行查询前根据条件修改sql
        return query.repalceAll("name=admin","1=1");
      }else{
        return query;
      }
    }
}

注意:以上代码我没有验证,因为没有 JPA3 的环境

9. 总结

最合适的方式我推荐的是 test1、test3、test6,也就是用 interface 的方式支持自定义实体类和用 Pageable 支持分页。

三. 代码的方式执行原生 SQL

这种方式繁琐一点,但是完全靠代码来实现,类似 JDBC 的方式,是最灵活的,可以实现各种复杂的需求。

我们首先在 dao(Repository)定义一个 test8() 函数

@Repository
public interface UserDao extends JpaRepository<UserEntity, String>, JpaSpecificationExecutor<UserEntity> {
   //这里要做一个关联查询,关联设备表和用户表,但是不用 @Query注解
   List test8(String name);
}

正常我们是需要编写一个类 实现这个接口,实现test8(),如下:

@Service
public class UserDaoService implements UserDao{
    @override
  List test8(String name){
    //......
  }
}

但是这样写是不合适的,因为如果这样写需要实现所有方法,包括 JpaRepository 和 JpaSpecificationExecutor 下有几十个方法,很显然这个不合适。

JPA 有一个约定,很好的解决这个问题,也就是在你定义一个实现类,类名是你定义的 Repository 类后加上后缀 Impl 就可以,而不需要显式的实现接口,也就是 ** 不需要 ** 写 implements UserDao ,如下

@Service
public class UserDaoImpl {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    private final EntityManager manager;

    public UserDaoImpl(EntityManager manager) {
        this.manager = manager;
    }

    public List test8(String name) {
        String nativeSql = "SELECT u.name,d.remark FROM d1_user u,d1_device d WHERE d.user_id=u.id and u.name like CONCAT('%', :name, '%')";
        Query query = manager.createNativeQuery(nativeSql);
        query.setParameter("name", name);
        List list = query.getResultList();
        //做一些相应的处理转换成我们需要的实体对象
        return list;
    }
}

看上去 UserDaoImpl 和 UserDao没有任何关系,但是 JPA 执行的时候会自动调用 UserDaoImpl 的 test8 方法,剩下就是开发者自己编写 SQL 语句,可以设置参数,冒号加名称表示一个参数变量。
这里弄了一个关联查询,我们返回的是一个 List ,里面是一个 Object,需要把 Object 转换成自定义的实体对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容