最近我在做一个新项目,由于我们项目组一直使用的是MongoDB数据库,所以新项目我就打算上SpringDataMongoDB尝试一下,虽然我早就用过了SpringDataJPA,对SpringData的相关CRUD和动态查询的封装也比较熟悉,但是自带的封装显然不能很好的满足我们的需求,本篇带大家讲述我所遇到的问题以及解决方案。 注:MongoRepositoryJPARepository都继承自PagingAndSortingRepository,除了对应的数据库不同之外,功能都基本相同,所以本文的二次封装也可以用于JPARepository上。1。我遇到的问题 问题一 在SpringData中可以通过继承MongoRepositoryJPARepository接口的方式获得CRUD和分页的能力,但是这种能力也仅仅满足基础的CRUD操作和分页,对于极其常用的两个操作比如:针对数据库某个字段进行更新和多条件查询,这个接口并没有提供。 准确的来说,多条件查询的能力是提供了,但是非常不宜用,它必须使用你的类做为查询条件,这个类的变量名还必须和数据库表中的字段名保持一致,这可以非常简单的让我们想到使用PO类当作这个查询条件。 但是在有些规范中,PO类应该是一个拥有全参构造器的不可变类,这使得先创建这个类然后对应的查询字段进行赋值的操作变得不可行,这里我举一个简单的例子,我拥有一个数据表的映射对象:User,这就是俗称的PO。Document(user)classUser(Idvalid:String,valaccount:String,valpwd:String,valname:String,)复制代码 然后我如果想要单独更新name这个字段时,我需要拥有整个User对象中的所有属性,因为Repository接口所提供的能力是把新增操作和更新操作放在一起的(save方法),每次更新都是所有字段的更新,这是我不愿意看到的,也是极其麻烦的。 接着就是多条件查询的问题,我们先来看下如果我想要使用多条件查询,它的参数是什么: 可以明显看到是一个叫Example的对象,如果我想使用,它应该是这样的:funtest(){valuserCssUser()user。name我要查询的参数具体值userRepository。findAll(Example。of(user))}复制代码 这里我定义了一个CssUser去当它的查询条件的类,而且这个类和User类的内容几乎一样,因为我的User类是一个全参构造器没办法直接创建一个空对象进行赋值,所以我不得不创建一个CssUser去当查询条件的类,对于程序员来讲,这很烦。 我想要的效果是什么样的呢?是这样的:funtest(){userRepository。listAll(Criteria。where(account)。is(admin)。and(name)。is(你的名字))}复制代码 通过lambda的方式直接获取到某个属性的名字,然后作为查询变量,然后跟着链式调用可以随便在里面加上各样的查询条件,例子中的Criteria类是Spring已经为我们做好的,但是Repository接口并没有提供它,所以我们需要一层封装。 问题二 从上面的例子中我们可以看到在组装查询条件时,需要硬编码进去字段名,这对于程序员来说,是很烦的。 所以我们应该使用lambda的特性,帮助我们去获取某一个类的字段名,通常是PO,因为它和数据库属性是一一对应的,整体要达到的有点像MybatisPLus的效果,大概是这样:funtest(){userRepository。listAll(Criteria。where(CssUser::account。mongoFiled())。is(admin)。and(CssUser::name。mongoFiled())。is(你的名字))}复制代码 当然我的这个效果还没有MybatisPLus的效果好,它可以直接省略。mongoFiled()这个操作,这是因为我只加了三四行代码就能达到这个效果,对我而言够用了,而MybatisPLus则是有一套相关支持。 虽然我这是Kotlin示例,但随后也会给出Java语法中的相关思路。2。Repository接口封装 先来谈谈对CRUD的增强,正常情况下,我们只需要使用一个接口继承MongoRepository接口,然后SpringData就会帮我们生成一个动态代理类,并声明为Bean,直接注入就可以使用了,就像这样(代码中的:语法是继承的意思):interfaceUserMongoRepository:MongoRepositoryUser,String{}复制代码 现在既然我们要对Repository进行增强,就需要再抽象出一个类,作为我们新的基类,之后的自己的业务类需要继承这个接口,而非原来的MongoRepository接口,当然,我们这个新的基类接口还会去继承MongoRepository接口,然后在接口中定义我们需要的新操作即可:NoRepositoryBeaninterfaceBaseMongoRepositoryT,ID:MongoRepositoryT,ID{funlistAll(condition:Criteria,pageable:Pageable):PageTfunupdateById(id:ID,update:Update):Long}复制代码 我创建了一个新的接口:BaseMongoRepository,用它来继承MongoRepository,接着定义我们需要的扩展的一些方法,这里我扩展类了两个方法:新的多条件分页方法和新的更新接口。 其中listAll方法的第一个参数Criteria是SpringData已经给我们提供好的类,它广泛运用于MongoTemplate里面,毕竟这层CRUD的封装底层其实还是MongoTemplate来操作。 除了继承接口外,我们还需要对这两个方法进行实现,再创建一个BaseMongoRepository的实现类去继承MongoRepository的实现类SimpleMongoRepository:classBaseMongoRepositoryClassT,ID(privatevalmetadata:MongoEntityInformationT,ID,privatevalmongoOperations:MongoOperations):SimpleMongoRepositoryT,ID(metadata,mongoOperations),BaseMongoRepositoryT,ID{privatevalclazz:ClassTmetadata。javaTypeoverridefunlistAll(condition:Criteria,pageable:Pageable):PageT{vallistmongoOperations。find(Query(condition)。with(pageable),this。clazz,metadata。collectionName)returnPageableExecutionUtils。getPage(list,pageable){mongoOperations。count(Query(condition)。limit(1)。skip(1),clazz,metadata。collectionName)}}overridefunupdateById(id:ID,update:Update):Long{if(update。updateObject。isEmpty())return0returnmongoOperations。updateFirst(Query()。addCriteria(Criteria。where(id)。is(id)),update,metadata。collectionName)。modifiedCount}}复制代码 其中BaseMongoRepositoryClass需要两个参数,这两个参数直接从SimpleMongoRepository里面拷贝过来然后通过构造再传递给SimpleMongoRepository即可,反正都是从自动注入里面来。 两个变量简单讲解一下都是什么意思:MongoEntityInformation:这个是MongoEntity的元信息,就是最上面用Document注解标记的PO类的元信息,我们可以通过它拿到PO类的类型和数据表的名字。MongoOperations:MongoTemplate的实现类,这个我想不用多谈。 接着就是方法实现,方法实现就是就是通过MongoTemplate操作了这个这个方法要做什么事,代码都比较简单因为不包含什么逻辑,熟悉MongoTemplate的一眼就可看懂。 接下来就是最重要的一步,没有这一步一切都是白费,还会造成项目启动失败,那就是把这个新的基类告诉Spring,这是新的基类,你可以在项目的入口中加上这一句注解:EnableMongoRepositories(basePackages〔com。xxx。〕,repositoryBaseClassBaseMongoRepositoryClass::class)classAdminApplicationfunmain(args:ArrayString){runApplication(args)}复制代码 指定一下repositoryBaseClass,这样生成动态代理的时候会以这个类为基类,我们动态代理类也就具有了我们定义的两个方法的能力了,使用中和原来的一样,只不过继承的接口不同罢了:interfaceUserRepository:BaseMongoRepositoryUser,String{}复制代码 到这一步,我们可以完成这个效果:funtest(){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类对象,那么我们直接给这个类加上扩展如下:funKProperty。mongoFiled():String{if(this。hasAnnotationId())returnidreturnthis。findAnnotationField()?。run{this。name。ifEmpty{thismongoFiled。name}}?:this。name}复制代码 这样在lambda调用下就可以再调用这个方法了,接着来看看方法内容。首先判断了是否存在ID注解,这个ID注解是用来标识Mongo的主键属性的注解,这种注解标识的变量在数据库中统一叫做id,所以这里我也返回这个名字。接着判断是否存在Field注解,它是用来标识数据库字段和类变量不一样的情况,如果出现这种情况,我们使用注解所标识的字段名。最后,以上两种情况排除后,我们直接使用这个字段的名字。 这样就可以达到如下效果了:funtest(){userRepository。listAll(Criteria。where(CssUser::account。mongoFiled())。is(admin)。and(CssUser::name。mongoFiled())。is(你的名字))}复制代码 接着我们可以来说说Java的做法,首先也需要一个方法通过lambda拿到字段名,这个方法网上有很多我不再赘述,但是拿到之后该怎么办呢? 你当然可以直接通过工具类的静态方法去拿,就像这样:funtest(){userRepository。listAll(Criteria。where(Util。getName(CssUser::account)。is(admin)。and(Util。getName(CssUser::name)。is(你的名字))}复制代码 可能到这一步看起来还是略微不雅,追求极致的小伙伴这个时候就可以再度发挥封装的本色,将Criteria类封装出一个新的查询条件类,比如叫Condition,然后将Criteria装在里面再封装一下查询时的相关常用方法,就像这样(注意此处的Funtion入参只是一个例子,实际应该是泛型):publicclassCondition{privateCriteriacriterianewCriteria();publicConditionwhere(FunctionString,Stringfunction,Stringvalue){criteria。andOperator(Criteria。where(Util。getName(function))。is(value));returnthis;}}复制代码 除了where方法你还可以继续封装gt、lt、or等常用方法,并且它们还能形成链式调用,最终的效果是这样的:publicstaticvoidmain(String〔〕args){CriteriacriterianewCondition()。where(CssUser::getName,你的名字)。where(CssUser::getAccount,admin);}复制代码 是不是更优雅了呢?4。最后 今天是满满的技术干货,希望Get到新技能的小伙伴可以积极的点赞,有什么问题都可以再评论区留言,我会积极对线的,下篇见。 作者:和耳朵 链接:https:juejin。cnpost7168133740093243423