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

news2024/11/16 17:36:09

本文正在参加「金石计划 . 瓜分6万现金大奖」

最近我在做一个新项目,由于我们项目组一直使用的是 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 到新技能的小伙伴可以积极的点赞,有什么问题都可以再评论区留言,我会积极对线的,下篇见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/31545.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第四章《类与对象》第3节:方法的重载

方法的重载是Java语言中一项非常重要的机制。Java语言因为有了重载机制,使得程序员定义和调用方法都变得更加轻松。 4.3.1方法重载的概念及实现原理 4.2小节的Person类中定义了计算2个整数之和的add()方法,如果程序员为add()方法传递两个double型参数,则会因参数类型不兼容…

期末前端web大作业——HTML+CSS+JavaScript仿京东购物商城网页制作(7页)

常见网页设计作业题材有 个人、 美食、 公司、 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 服装、 体育、 化妆品、 物流、 环保、 书籍、 婚纱、 游戏、 节日、 戒烟、 电影、 摄影、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A…

聊一聊我对Restful的理解

概念 REST原则提倡按照HTTP的语义使用HTTP&#xff0c;如果一个系统符合REST原则&#xff0c;我们就说这个系统是Restful风格的。Restful是Web API设计中非常重要的一个概念&#xff0c;但是很多开发人员对于Restful的理解存在误区。 什么是Restful 在说什么是Restful 之前&…

数据结构之基数排序

基数排序 先把各个数以个位数不同分到不同的队列中 如果一个队列多个元素用链表连起来 第一趟分配 然后进行第一趟收集 应为我们想得到递减 所以我们从个位数高到低收集 然后第二趟分配 根据第一趟得到的结果 以十位数分配 这里注意&#xff01; 因为第一趟按个位分的&am…

ImmunoChemistry艾美捷高级钙素AM细胞活力试剂盒方案

ImmunoChemistry艾美捷ICT的Advanced Calcein AM Cell Viability Kit将Calcein AM与7-AAD相结合&#xff0c;可轻松同时标记单个样本中的活细胞、膜受损细胞和死细胞。钙黄绿素AM用于检测绿色荧光的活细胞&#xff0c;而7-AAD用于检测红色荧光的坏死或晚期凋亡细胞。可以使用流…

超详细curl新增支持openssl(https协议)支持

1、问题环境&#xff1a; os&#xff1a;Linux kali 5.5.0-kali2-amd64 #1 SMP Debian 5.5.17-1kali1 (2020-04-21) x86_64 GNU/Linux 2、涉及组件&#xff1a; curl当前版本&#xff1a;curl 7.85.0 openssl当前版本&#xff1a;OpenSSL 3.0.7 1 Nov 2022 (Library: OpenS…

学生个人网页设计作品 学生个人网页模板 简单个人主页成品 个人网页制作 HTML学生个人网站作业设计

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

遥感SCI期刊汇总

1. BOLETIM DE CIENCIAS GEODESICAS 《大地测量科学通报》http://ojs.c3sl.ufpr.br/ojs2/index.php/bcgSemiannual &#xff08;注&#xff1a;2008年开始被SCI收录&#xff09;ISSN: 1413-4853UNIV FEDERAL PARANA, CENTRO POLITECNICO, UFPR CENTRO POLITECNICO, CURSO POS…

如何使用云服务器以及宝塔面板快速搭建discuz论坛网站?

前面介绍了很多搭建个人网站&#xff0c;云网盘的方法&#xff0c;这篇文章将介绍使用discuz搭建一个论坛网站&#xff0c;采用的方式为轻量应用服务器搭配宝塔面板一键式部署discuz论坛网站&#xff0c;感兴趣的小伙伴可以跟着我一起搭建起来&#xff01; 说明&#xff1a; 云…

无线传感器网络:数据链路层,MAC

文章目录FramingByte CountFlag Byte MethodByte StuffingMedium Access Control (MAC)Static Channel AllocationDynamic Channel AllocationIndependent TrafficSingle ChannelObservable CollisionsContinuous or Slotted TimeCarrier Sense or No Carrier SensePure ALOHAS…

react的useState源码分析

前言 简单说下为什么React选择函数式组件&#xff0c;主要是class组件比较冗余、生命周期函数写法不友好&#xff0c;骚写法多&#xff0c;functional组件更符合React编程思想等等等。更具体的可以拜读dan大神的blog。其中Function components capture the rendered values这句…

JWT有状态登陆与无状态登陆

单点登录与JWT JWT 全称&#xff1a; Json Web Token 。作用&#xff1a; JWT 的作用是 用户授权(Authorization) &#xff0c;而不是用户的身份认证(Authentication) 。用户认证 指的是使用用户名、密码来验证当前用户的身份&#xff0c;即用户登录。用户授权 指用户登录成功后…

「从零单排canal 07」 parser模块源码解析

本文将对canal的binlog订阅模块parser进行分析。 parser模块(绿色部分)在整个系统中的角色如下图所示&#xff0c;用来订阅binlog事件&#xff0c;然后通过sink投递到store. parser模块应该来说是整个项目里面比较复杂的模块&#xff0c;代码非常多。 因此&#xff0c;本文根…

Nmap爆破MySQL弱口令漏洞:解决报错Accounts: No valid accounts found

nmap工具不仅仅能扫描&#xff0c;也可以暴力破解mysql&#xff0c;ftp&#xff0c;telnet等服务。 看到这里不要怀疑&#xff0c;在kali系统中查一下到底支持哪些暴力破解功能&#xff0c;命令如下 ls /usr/share/nmap/scripts |grep brute.nse 查询结果为 afp-brute.nse …

镜像底层原理详解和基于Docker file创建镜像

目录 一、镜像底层原理 1.联合文件系统(UnionFS) 2.镜像加载原理 3.为什么Docker里的centos的大小才200M? 二、Dockerfile 1.简介 2.Dockerfile操作常用命令 &#xff08;1&#xff09;FORM 镜像 &#xff08;2&#xff09;MAINTAINER 维护人信息 &#xff08;3&…

Vue的devtools安装教程

devtools是一个便于开发者调试Vue代码的插件 先确保你已经安装了node.js 点击此处去github上拉取工具包 安装yarn&#xff08;用npm在打包的时候会失败&#xff0c;使用yarn可以打包成功&#xff09; ① cmd输入&#xff1a;npm install -g yarn 全局安装yarn包管理工具   …

Nature Plants|植物基因组测序20年回顾与展望:三代HiFi基因组时代

2021年11月29日&#xff0c;美国密歇根州立大学在《Nature Plants》期刊在线发表题为“Representation and participation across 20 years of plant genomesequencing”综述&#xff0c;系统阐述了在过去的20年间&#xff0c;对陆地植物基因组学组装质量、已测序物种的分类和地…

Java的几大常用类

一、Object类 超类、基类&#xff0c;所有类的直接或者间接父类&#xff0c;位于继承树的最顶层。 任何类&#xff0c;如果没有写 extends 显示继承某个类&#xff0c;都直接默认继承 Object 类&#xff0c;否则为间接继承。 Object 类中所定义的方法&#xff0c;是所有对象…

DeeplabV3实战:基于tensorflow搭建DeeplabV3实现语义分割任务

任务描述: 语义分割是一种典型的计算机视觉问题,其是将一些图像作为输入并将它们转换为具有突出显示的感兴趣区域的掩模,即图像中的每个像素根据其所属的感兴趣对象被分配类别。如下图中左图所示,其语义是人骑自行车,语义分割的结果如右图所示,粉红色代表人,绿色代表自行…

狂神的MySQL(1)

01、什么是数据库&#xff0c;为什么要学习数据库 javaEE&#xff1a;企业级java开发 Web 前端&#xff08;页面&#xff1a;展示&#xff0c;数据&#xff01;&#xff09; 后台&#xff08;连接点&#xff1a;连接数据库JDBC&#xff0c;连接前端&#xff08;控制&#xf…