您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

二次封装 Spring Data JPA/MongoDB,打造更易用的数据访问层

时间:2022-11-22 13:29:25  来源:掘金  作者:和耳朵

 

最近我在做一个新项目,由于我们项目组一直使用的是 MongoDB 数据库,所以新项目我就打算上 Spring Data MongoDB 尝试一下,虽然我早就用过了 Spring Data JPA,对 Spring Data 的相关 CRUD 和 动态查询的封装也比较熟悉,但是自带的封装显然不能很好的满足我们的需求,本篇带大家讲述我所遇到的问题以及解决方案。

注: MongoRepository / JPARepository 都继承自
PagingAndSortingRepository,除了对应的数据库不同之外,功能都基本相同,所以本文的二次封装也可以用于 JPARepository 上。

1. 我遇到的问题

问题一

在 Spring Data 中可以通过继承 MongoRepository / JPARepository 接口的方式获得 CRUD 和 分页的能力,但是这种能力也仅仅满足基础的 CRUD 操作和 分页,对于极其常用的两个操作比如:针对数据库某个字段进行更新 和 多条件查询,这个接口并没有提供。

准确的来说,多条件查询的能力是提供了,但是非常不宜用,它必须使用你的类做为查询条件,这个类的变量名还必须和数据库表中的字段名保持一致,这可以非常简单的让我们想到使用 PO 类当作这个查询条件。

但是在有些规范中,PO 类应该是一个拥有全参构造器的不可变类,这使得先创建这个类然后对应的查询字段进行赋值的操作变得不可行,这里我举一个简单的例子,我拥有一个数据表的映射对象:User,这就是俗称的 PO

@Document("user")
class User (

    @Id
    val id : String,
​
    val account : String,
​
    val pwd : String,
​
    val name : String,
)
复制代码

然后我如果想要单独更新 name 这个字段时,我需要拥有整个 User 对象中的所有属性,因为 Repository 接口所提供的能力是把新增操作和更新操作放在一起的 (save 方法),每次更新都是所有字段的更新,这是我不愿意看到的,也是极其麻烦的。

接着就是多条件查询的问题,我们先来看下如果我想要使用多条件查询,它的参数是什么:

 

可以明显看到是一个叫 Example 的对象,如果我想使用,它应该是这样的:

    fun test() {
        
        val user = cssUser()
        
        user.name = "我要查询的参数具体值"
​
        userRepository.findAll(Example.of(user))
    }
复制代码

这里我定义了一个 CssUser 去当它的查询条件的类,而且这个类和 User 类的内容几乎一样,因为我的 User 类是一个全参构造器没办法直接创建一个空对象进行赋值,所以我不得不创建一个 CssUser 去当查询条件的类,对于程序员来讲,这很烦

我想要的效果是什么样的呢?是这样的:

    fun test() {
​
        userRepository.listAll(Criteria
                                   .where("account").`is`("admin")
                                   .and("name").`is`("你的名字")
        )
​
    }
复制代码

通过 lambda 的方式直接获取到某个属性的名字,然后作为查询变量,然后跟着链式调用可以随便在里面加上各样的查询条件,例子中的 Criteria 类是 Spring 已经为我们做好的,但是 Repository 接口并没有提供它,所以我们需要一层封装。

问题二

从上面的例子中我们可以看到在组装查询条件时,需要硬编码进去字段名,这对于程序员来说,是很烦的

所以我们应该使用 lambda 的特性,帮助我们去获取某一个类的字段名,通常是 PO,因为它和数据库属性是一一对应的,整体要达到的有点像 MyBatis-PLus 的效果,大概是这样:

    fun test() {
​
        userRepository.listAll(Criteria
                                   .where(CssUser::account.mongoFiled()).`is`("admin")
                                   .and(CssUser::name.mongoFiled()).`is`("你的名字")
        )
​
    }
复制代码

当然我的这个效果还没有 Mybatis-PLus 的效果好,它可以直接省略 .mongoFiled() 这个操作,这是因为我只加了三四行代码就能达到这个效果,对我而言够用了,而 Mybatis-PLus 则是有一套相关支持。

虽然我这是 Kotlin 示例,但随后也会给出 JAVA 语法中的相关思路。

2. Repository 接口封装

先来谈谈对 CRUD 的增强,正常情况下,我们只需要使用一个接口继承 MongoRepository 接口,然后 Spring Data 就会帮我们生成一个动态代理类,并声明为 Bean,直接注入就可以使用了,就像这样(代码中的 :语法是继承的意思):

interface UserMongoRepository : MongoRepository<User, String> {
​
}
复制代码

现在既然我们要对 Repository 进行增强,就需要再抽象出一个类,作为我们新的基类,之后的自己的业务类需要继承这个接口,而非原来的 MongoRepository 接口,当然,我们这个新的基类接口还会去继承 MongoRepository 接口,然后在接口中定义我们需要的新操作即可:

@NoRepositoryBean
interface BaseMongoRepository<T, ID> : MongoRepository<T, ID> {
​
    fun listAll(condition: Criteria, pageable: Pageable): Page<T>
​
    fun updateById(id: ID, update: Update): Long
}
复制代码

我创建了一个新的接口:BaseMongoRepository,用它来继承 MongoRepository,接着定义我们需要的扩展的一些方法,这里我扩展类了两个方法:新的多条件分页方法和新的更新接口。

其中 listAll 方法的第一个参数 Criteria 是 Spring Data 已经给我们提供好的类,它广泛运用于 MongoTemplate 里面,毕竟这层 CRUD 的封装底层其实还是 MongoTemplate 来操作。

除了继承接口外,我们还需要对这两个方法进行实现,再创建一个 BaseMongoRepository 的实现类去继承 MongoRepository 的实现类——SimpleMongoRepository:

class BaseMongoRepositoryClass<T, ID>(
    private val metadata: MongoEntityInformation<T, ID>,
    private val mongoOperations: MongoOperations
) :
    SimpleMongoRepository<T, ID>(metadata, mongoOperations), BaseMongoRepository<T, ID> {
​
    private val clazz: Class<T> = metadata.javaType
​
    override fun listAll(condition: Criteria, pageable: Pageable): Page<T> {
        val list = mongoOperations.find(Query(condition).with(pageable), this.clazz, metadata.collectionName)
​
        return PageableExecutionUtils.getPage(list, pageable) {
            mongoOperations
                .count(
                    Query(condition).limit(-1).skip(-1),
                    clazz,
                    metadata.collectionName
                )
        }
    }
​
    override fun updateById(id: ID, update: Update): Long {
        if (update.updateObject.isEmpty()) return 0
        return mongoOperations.updateFirst(
            Query().addCriteria(Criteria.where("_id").`is`(id)),
            update,
            metadata.collectionName
        ).modifiedCount
    }
​
​
}
复制代码

其中 BaseMongoRepositoryClass 需要两个参数,这两个参数直接从 SimpleMongoRepository 里面拷贝过来然后通过构造再传递给 SimpleMongoRepository 即可,反正都是从自动注入里面来。

两个变量简单讲解一下都是什么意思:

  1. MongoEntityInformation:这个是 MongoEntity 的元信息,就是最上面用 @Document 注解标记的 PO 类的元信息,我们可以通过它拿到 PO 类的类型和数据表的名字。
  2. MongoOperations:MongoTemplate 的实现类,这个我想不用多谈。

接着就是方法实现,方法实现就是就是通过 MongoTemplate 操作了这个这个方法要做什么事,代码都比较简单因为不包含什么逻辑,熟悉 MongoTemplate 的一眼就可看懂。

接下来就是最重要的一步,没有这一步一切都是白费,还会造成项目启动失败,那就是把这个新的基类告诉 Spring,这是新的基类,你可以在项目的入口中加上这一句注解:

@EnableMongoRepositories(basePackages = ["com.xxx.*"], repositoryBaseClass = BaseMongoRepositoryClass::class)
class AdminApplication
​
fun mAIn(args: Array<String>) {
    runApplication<AdminApplication>(*args)
}
复制代码

指定一下 repositoryBaseClass,这样生成动态代理的时候会以这个类为基类,我们动态代理类也就具有了我们定义的两个方法的能力了,使用中和原来的一样,只不过继承的接口不同罢了:

interface UserRepository : BaseMongoRepository<User, String> {
​
}
复制代码

到这一步,我们可以完成这个效果:

    fun test() {
​
        userRepository.listAll(Criteria
                                   .where("account").`is`("admin")
                                   .and("name").`is`("你的名字")
        )
​
    }
复制代码

3. 实体类变量进行 lambda 封装

接下来是对实体变量进行 lambda 封装,这个东西我觉得可以分为 Kotlin 和 Java 两个版本来说,两者各有千秋。

先来说说Kotlin,因为 Kotlin 自身的语言特性的关系,实现起来比较简单,但也会拖一个尾巴,Kotlin 具有一个扩展函数的能力,简单点说就是直接给某个类加上一些自定义方法,比如 String 我们可以在不继承的情况下直接给 String 类加上一个新的方法,然后它就会出现在 String 对象可调用的函数列表中。

所以我们如果想要 User::account.mongoFiled() 这种效果,就得先知道 User::account 返回值是什么,在 Kotlin 中,它的返回值是一个 KProperty 类对象,那么我们直接给这个类加上扩展如下:

fun KProperty<*>.mongoFiled(): String {
    if (this.hasAnnotation<Id>()) return "_id"
    return this.findAnnotation<Field>()?.run {
        this.name.ifEmpty { this@mongoFiled.name }
    } ?: this.name
}
复制代码

这样在 lambda 调用下就可以再调用这个方法了,接着来看看方法内容。

  1. 首先判断了是否存在 ID 注解,这个 ID 注解是用来标识 Mongo 的主键属性的注解,这种注解标识的变量在数据库中统一叫做 "_id",所以这里我也返回这个名字。
  2. 接着判断是否存在 Field 注解,它是用来标识数据库字段和类变量不一样的情况,如果出现这种情况,我们使用注解所标识的字段名。
  3. 最后,以上两种情况排除后,我们直接使用这个字段的名字。

这样就可以达到如下效果了:

    fun test() {
​
        userRepository.listAll(Criteria
                                   .where(CssUser::account.mongoFiled()).`is`("admin")
                                   .and(CssUser::name.mongoFiled()).`is`("你的名字")
        )
​
    }
复制代码

接着我们可以来说说 Java 的做法,首先也需要一个方法通过 lambda 拿到字段名,这个方法网上有很多我不再赘述,但是拿到之后该怎么办呢?

你当然可以直接通过工具类的静态方法去拿,就像这样:

    fun test() {
​
        userRepository.listAll(Criteria
                                   .where(Util.getName(CssUser::account).`is`("admin")
                                   .and(Util.getName(CssUser::name).`is`("你的名字")
        )
​
    }
复制代码

可能到这一步看起来还是略微不雅,追求极致的小伙伴这个时候就可以再度发挥封装的本色,将 Criteria 类封装出一个新的查询条件类,比如叫 Condition,然后将 Criteria 装在里面再封装一下查询时的相关常用方法,就像这样(注意此处的 Funtion 入参只是一个例子,实际应该是泛型):

public class Condition {
    
    private Criteria criteria = new Criteria();
​
    public Condition where(Function<String, String> function, String value) {
        criteria.andOperator(Criteria.where(Util.getName(function)).is(value));
        return this;
    }
}
复制代码

除了 where 方法你还可以继续封装 gt、lt、or 等常用方法,并且它们还能形成链式调用,最终的效果是这样的:

    public static void main(String[] args) {
        Criteria criteria = new Condition()
                .where(CssUser::getName, "你的名字")
                .where(CssUser::getAccount, "admin");
    }
复制代码

是不是更优雅了呢?

4. 最后

今天是满满的技术干货,希望 Get 到新技能的小伙伴可以积极的点赞,有什么问题都可以再评论区留言,我会积极对线的,下篇见。

作者:和耳朵
链接:
https://juejin.cn/post/7168133740093243423



Tags:Spring   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  Search: Spring  点击:(55)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  Search: Spring  点击:(39)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  Search: Spring  点击:(19)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  Search: Spring  点击:(88)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19  Search: Spring  点击:(86)  评论:(0)  加入收藏
Spring Boot2.0深度实践 核心原理拆解+源码分析
Spring Boot2.0深度实践:核心原理拆解与源码分析一、引言Spring Boot是一个基于Java的轻量级框架,它简化了Spring应用程序的创建过程,使得开发者能够快速搭建一个可运行的应用...【详细内容】
2024-01-15  Search: Spring  点击:(96)  评论:(0)  加入收藏
SpringBoot3+Vue3 开发高并发秒杀抢购系统
开发高并发秒杀抢购系统:使用SpringBoot3+Vue3的实践之旅随着互联网技术的发展,电商行业对秒杀抢购系统的需求越来越高。为了满足这种高并发、高流量的场景,我们决定使用Spring...【详细内容】
2024-01-14  Search: Spring  点击:(91)  评论:(0)  加入收藏
Spring Boot 3.0是什么?
Spring Boot 3.0是一款基于Java的开源框架,用于简化Spring应用程序的构建和开发过程。与之前的版本相比,Spring Boot 3.0在多个方面进行了改进和增强,使其更加易用、高效和灵活...【详细内容】
2024-01-11  Search: Spring  点击:(133)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11  Search: Spring  点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  Search: Spring  点击:(119)  评论:(0)  加入收藏
▌简易百科推荐
Qt与Flutter:在跨平台UI框架中哪个更受欢迎?
在跨平台UI框架领域,Qt和Flutter是两个备受瞩目的选择。它们各自具有独特的优势,也各自有着广泛的应用场景。本文将对Qt和Flutter进行详细的比较,以探讨在跨平台UI框架中哪个更...【详细内容】
2024-04-12  刘长伟    Tags:UI框架   点击:(1)  评论:(0)  加入收藏
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(19)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(55)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(51)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(68)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(88)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
站内最新
站内热门
站内头条