spring boot实战之JPA

公司的项目中很大一部分属于内部平台,所以对性能的要求没有那么高,开发速度反而更重要,因此在搭建基础框架时选择使用JPA,没有使用mybitis,当然其中也有一部分原因是之前一直使用hibernate,对mybitis不太熟悉_

一、配置JPA

1、添加maven依赖

<!-- jpa -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

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

2、 添加数据库配置

spring.jpa.database = MYSQL
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
# Show or not log for each sql query
spring.jpa.show-sql = true
spring.datasource.url=jdbc:mysql://localhost:3306/base?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.max-active=3
spring.datasource.max-idle=1
spring.datasource.min-idle=1
spring.datasource.initial-size=1

3、dao继承上层类

public interface UserDao  extends BaseDao<User, Integer>{
    User findByUsernameAndDel(String username, int del);
}

@NoRepositoryBean
public interface BaseDao<T,ID extends Serializable> extends JpaSpecificationExecutor<T>,JpaRepository<T, ID>{

}

为了便于使用,提取了一个BaseDao,需要注意的是要在上层类上添加@ NoRepositoryBean注解。BaseDao继承JpaRepository和JpaSpecificationExecutor,JpaRepository提供了基本的crud等查询方法,JpaSpecificationExecutor提供了对复杂查询的支持。

完成了以上三步,已经可以在service内注入dao,通过dao进行数据库curd等操作。

二、JPA查询

1、 根据方法名实现查询

//根据用户名和标记删除字段查询对应的用户信息
User findByUsernameAndDel(String username, int del);

//根据code查询对应的角色
Role findByCode(String code);

//根据id集合查询对应的角色集合
Set<Role> findByIdIn(Set<Integer> roleIds);

//根据用户id,查询用户角色关系记录
List<UserRole> findByUserId(int uid);

简单查询可以通过以上方式方便的实现,简化了很多dao层的代码,使用着还是很爽的,具体规则比较简单,基本上就是findBy开始,后续跟上实体属性,中间配以And、Or、In、like等组成方法名,也就是用方法名来描述查询规则。如果是嵌套对象,可以通过“_"来区分子对象的属性,比如findByCompany_name(String name)就是以子对象company内的name属性为查询条件。常用查询关键字如下:

  • And --- 等价于 SQL 中的 and 关键字,比如findByUsernameAndPassword(String user, Striang pwd);
  • Or --- 等价于 SQL 中的 or 关键字,比如findByUsernameOrAddress(String user, String addr);
  • Between --- 等价于 SQL 中的 between 关键字,比如 findBySalaryBetween(int max, int min);
  • LessThan --- 等价于 SQL 中的 "<",比如 findBySalaryLessThan(int max);
  • GreaterThan --- 等价于 SQL 中的">",比如 findBySalaryGreaterThan(int min);
  • IsNull --- 等价于 SQL 中的 "is null",比如 findByUsernameIsNull();
  • IsNotNull --- 等价于 SQL 中的 "is not null",比如 findByUsernameIsNotNull();
  • NotNull --- 与 IsNotNull 等价;
  • Like --- 等价于 SQL 中的 "like",比如 findByUsernameLike(String user);
  • NotLike --- 等价于 SQL 中的 "not like",比如 findByUsernameNotLike(String user);
  • OrderBy --- 等价于 SQL 中的 "order by",比如 findByUsernameOrderBySalaryAsc(String user);
  • Not --- 等价于 SQL 中的 "! =",比如 findByUsernameNot(String user);
  • In --- 等价于 SQL 中的 "in",比如findByUsernameIn(Collection<String> userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;
  • NotIn --- 等价于 SQL 中的 "not in",比如 findByUsernameNotIn(Collection<String> userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

2、使用@Query查询

类HQL语句

@Query("select u from User u where u.username = ?1") 
public AccountInfo findByAccountId(String username); 

在@Query内直接书写HQL语句即可,参数可通过"?1,?2"这样的方式设置,下标从1开始。

原生sql查询

@Query(value="select perm.* from role_permission rp left join permission perm on rp.permission_id=perm.id where rp.role_id in(?1);",nativeQuery=true)
List<Permission> listByRoleIds(Set<Integer> roles);

//根据userId删除用户角色关系
@Query(value = "delete from user_role where user_id=?1 ", nativeQuery = true)  
@Modifying
void deleleByUserId(int uid);

使用也比较简单,将@Query内的nativeQuery设置为true即可。写SQL语句时,可以现在本地mysql客户端上测试号SQL语句的正确性,当需要索引时,创建合适的索引。在此基础上看下SQL的性能,如不理想,需调整SQL,可使用explain对SQL语句进行分析,查询执行逻辑,针对性优化。

新增、修改
直接调用dao的save方法,支持单个保存和批量保存。

删除
直接调用dao的delete方法,支持根据id、对象、对象集合等删除方式,使用时查看下提示方法就可以了。

3、分页查询

不带条件分页查询

Pageable pageable = new PageRequest(0, 10, new Sort(Direction.DESC, "updateTime"));
Page<User> page = userDao.findAll(pageable);
  • 使用PageRequest构建分页请求对象,页码下标从0开始

多条件复杂分页查询
带条件分页查询有两种方式:

  1. 使用原生SQL进行分页查询,但是前提是多个查询条件必须同时存在,不能有不生效的条件,比如用户列表,用户姓名可以不作为过滤条件,这种情况原生SQL就不适用了,需要使用下面第二种方式
  2. 使用Specification进行复杂查询,示例代码如下:
public Page<User> listByPage(final Map<String, String> params,Pageable pageable){
    Specification<User> spec = new Specification<User>() {  
        @Override
        public Predicate toPredicate(Root<User> root,CriteriaQuery<?> query,CriteriaBuilder cb) {
            List<Predicate> list = new ArrayList<Predicate>();  
            String type = params.get("type");
            String status= params.get("status");
            String username = params.get("username");
            String name = params.get("name");
            
            if(StringUtils.isNotBlank(type)){  
                list.add(cb.equal(root.get("type").as(Integer.class), NumberUtils.toInt(type)));  
            }  
            if(StringUtils.isNotBlank(status)){  
                list.add(cb.equal(root.get("status").as(Integer.class), NumberUtils.toInt(status)));  
            }
            if(StringUtils.isNotBlank(username)){  
                list.add(cb.like(root.get("username").as(String.class), String.format("%%%s%%", username)));  
            }  
            if(StringUtils.isNotBlank(name)){  
                list.add(cb.like(root.get("name").as(String.class), String.format("%%%s%%", name)));   
            }
            list.add(cb.equal(root.get("del"), Constants.DEL_NO));
            Predicate[] p = new Predicate[list.size()];  
            return cb.and(list.toArray(p));  
            
            //in条件查询
            /*List<Integer> ids = Lists.newArrayList();
            ids.add(1);
            ids.add(2);
            In<Integer> in = cb.in(root.get("id").as(Integer.class));
            in.value(1);
            in.value(2);
            return cb.or(in);*/
        }  
    };  
    Page<User> page = userDao.findAll(spec, pageable);

根据以上示例,基本满足了常用的查询需求,更多情况可根据规则尝试一下即可,也可百度搜索下JPA Specification,有很多教程。

简化多条件分页查询
使用Specification需要每次都写一大段模板代码,使用起来还是比较繁琐,使用入门也有些难度,基于此,在service层的公共代码出对查询进行了部分封装,简化常见多条件分页查询。

/**
 * 分页多条件查询
 * 注:多个条件间是and关系 & 参数是属性对应的类型
 * @author yangwk
 * @time 2017年8月1日 下午3:50:46
 * @param params {"username:like":"test"} 键的格式为字段名:过滤方式,过滤方式见{@code QueryTypeEnum}
 * @param pageable 分页信息 new PageRequest(page, size,new Sort(Direction.DESC, "updateTime"))
 * @return
 */
Page<T> list(Map<String, Object> params,Pageable pageable);

@Override
public Page<T> list(final Map<String, Object> params,Pageable pageable){
    Specification<T> spec = new Specification<T>() {  
        @Override
        public Predicate toPredicate(Root<T> root,CriteriaQuery<?> query,CriteriaBuilder cb) {
            List<Predicate> list = new ArrayList<Predicate>();
            for(Entry<String, Object> entry : params.entrySet()){
                Object value = entry.getValue();
                if(value == null || StringUtils.isBlank(value.toString())){
                    continue;
                }
                String key = entry.getKey();
                String[] arr = key.split(":");
                Predicate predicate = getPredicate(arr,value,root,cb);
                list.add(predicate);
            }
            Predicate[] p = new Predicate[list.size()];  
            return cb.and(list.toArray(p));  
        }
    };  
    Page<T> page = getDAO().findAll(spec, pageable);
    return page;
}


private Predicate getPredicate(String[] arr, Object value,
        Root<T> root, CriteriaBuilder cb) {
    if(arr.length == 1){
        return cb.equal(root.get(arr[0]).as(value.getClass()), value);  
    }
    if(QueryTypeEnum.like.name().equals(arr[1])){
        return cb.like(root.get(arr[0]).as(String.class), String.format("%%%s%%", value));
    }
    if(QueryTypeEnum.ne.name().equals(arr[1])){
        return cb.notEqual(root.get(arr[0]).as(value.getClass()), value);
    }
    if(QueryTypeEnum.lt.name().equals(arr[1])){
        return getLessThanPredicate(arr,value,root,cb);
    }
    if(QueryTypeEnum.lte.name().equals(arr[1])){
        return getLessThanOrEqualToPredicate(arr,value,root,cb);
    }
    if(QueryTypeEnum.gt.name().equals(arr[1])){
        return getGreaterThanPredicate(arr,value,root,cb);
    }
    if(QueryTypeEnum.gte.name().equals(arr[1])){
        return getGreaterThanOrEqualToPredicate(arr,value,root,cb);
    }
    throw new UnsupportedOperationException(String.format("不支持的查询类型[%s]",arr[1]));
}

private Predicate getLessThanPredicate(String[] arr, Object value,
        Root<T> root, CriteriaBuilder cb) {
    if(Integer.class == value.getClass()){
        return cb.lessThan(root.get(arr[0]).as(Integer.class), (int)value);
    }
    if(Long.class == value.getClass()){
        return cb.lessThan(root.get(arr[0]).as(Long.class), (long)value);
    }
    if(Double.class == value.getClass()){
        return cb.lessThan(root.get(arr[0]).as(Double.class), (double)value);
    }
    if(Float.class == value.getClass()){
        return cb.lessThan(root.get(arr[0]).as(Float.class), (float)value);
    }
    if(Timestamp.class == value.getClass()){
        return cb.lessThan(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
    }
    if(Date.class == value.getClass()){
        return cb.lessThan(root.get(arr[0]).as(Date.class), (Date)value);
    }
    return cb.lessThan(root.get(arr[0]).as(String.class), String.valueOf(value));
}

private Predicate getLessThanOrEqualToPredicate(String[] arr,
        Object value, Root<T> root, CriteriaBuilder cb) {
    if(Integer.class == value.getClass()){
        return cb.lessThanOrEqualTo(root.get(arr[0]).as(Integer.class), (int)value);
    }
    if(Long.class == value.getClass()){
        return cb.lessThanOrEqualTo(root.get(arr[0]).as(Long.class), (long)value);
    }
    if(Double.class == value.getClass()){
        return cb.lessThanOrEqualTo(root.get(arr[0]).as(Double.class), (double)value);
    }
    if(Float.class == value.getClass()){
        return cb.lessThanOrEqualTo(root.get(arr[0]).as(Float.class), (float)value);
    }
    if(Timestamp.class == value.getClass()){
        return cb.lessThanOrEqualTo(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
    }
    if(Date.class == value.getClass()){
        return cb.lessThanOrEqualTo(root.get(arr[0]).as(Date.class), (Date)value);
    }
    return cb.lessThanOrEqualTo(root.get(arr[0]).as(String.class), String.valueOf(value));
}

private Predicate getGreaterThanPredicate(String[] arr,
        Object value, Root<T> root, CriteriaBuilder cb) {
    if(Integer.class == value.getClass()){
        return cb.greaterThan(root.get(arr[0]).as(Integer.class), (int)value);
    }
    if(Long.class == value.getClass()){
        return cb.greaterThan(root.get(arr[0]).as(Long.class), (long)value);
    }
    if(Double.class == value.getClass()){
        return cb.greaterThan(root.get(arr[0]).as(Double.class), (double)value);
    }
    if(Float.class == value.getClass()){
        return cb.greaterThan(root.get(arr[0]).as(Float.class), (float)value);
    }
    if(Timestamp.class == value.getClass()){
        return cb.greaterThan(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
    }
    if(Date.class == value.getClass()){
        return cb.greaterThan(root.get(arr[0]).as(Date.class), (Date)value);
    }
    return cb.greaterThan(root.get(arr[0]).as(String.class), String.valueOf(value));
}

private Predicate getGreaterThanOrEqualToPredicate(String[] arr,Object value,
        Root<T> root, CriteriaBuilder cb) {
    if(Integer.class == value.getClass()){
        return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Integer.class), (int)value);
    }
    if(Long.class == value.getClass()){
        return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Long.class), (long)value);
    }
    if(Double.class == value.getClass()){
        return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Double.class), (double)value);
    }
    if(Float.class == value.getClass()){
        return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Float.class), (float)value);
    }
    if(Timestamp.class == value.getClass()){
        return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Timestamp.class), (Timestamp)value);
    }
    if(Date.class == value.getClass()){
        return cb.greaterThanOrEqualTo(root.get(arr[0]).as(Date.class), (Date)value);
    }
    return cb.lessThanOrEqualTo(root.get(arr[0]).as(String.class), String.valueOf(value));
}  

@ApiModel(value="查询条件支持的过滤方式")
public enum QueryTypeEnum {
    like,
    equal,
    ne,
    lt,
    lte,
    gt,
    gte
}

//使用示例
Map<String, Object> params = Maps.newHashMap();
params.put("type", type);
params.put("status", status);
params.put("username:like", username);
params.put("name:like", name);
Page<User> rs = this.userService.list(params, new PageRequest(page, size, new Sort(Direction.DESC, "updateTime")));

提前公共list方法,查询条件在map内设置,查询条件在key内设置,这样大部分的查询请求就可以不再关注Specification的语法,不用写那一大段的复杂代码了

小结

  1. 配置JPA很简单,添加maven依赖,配置数据库连接信息,dao继承上层类即可在service内注入dao,进行crud等操作
  2. JPA提供了简便的根据方法名称进行查询的方式,使用难度很低
  3. JPA通过@Query注解,支持类HQL语句查询;也可以使用原生SQL查询,只需要将nativeQuery属性设置为true即可
  4. 无条件分页查询可通过自带的findAll方法即可
  5. 多条件分页查询,有两种实现方式,当每个条件都是必选时,可使用@query带分页条件来实现;当有可选条件时,需要使用Specification来实现
  6. 为了简化常见的多个可选条件分页查询的代码,在service层提供了一个上层方法,以map的方式设置查询条件,大部分情况下不需要程序员再关注Specification的语法,降低使用难度

本人搭建好的spring boot web后端开发框架已上传至GitHub,欢迎吐槽!
https://github.com/q7322068/rest-base,已用于多个正式项目,当前可能因为版本问题不是很完善,后续持续优化,希望你能有所收获!

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

推荐阅读更多精彩内容