浅谈Fenix API的原理和使用

一、简介

Fenix 简称菲尼克斯是一个为了解决复杂动态 SQL (JPQL) 而生的 Spring Data JPA 扩展库,目的是辅助开发者更方便快捷的书写复杂、动态且易于维护的 SQL,支持 XML、Java 链式 API 和动态条件注解等四种方式来书写动态 SQL。

Fenix的选择是在原有的Spring Data JPA库上做了增强,提供了各种高效率的API方法和Java ApI等方式的链式操作;之前基于Mybatis中的运用,需要在xml中编写JPQL语言,而为了简便开发,Fenix在xml中引入了MVEL表达式和模板引擎的语法来灵活编写和渲染动态SQL。对于为了避免在xml中出现重复的SQL,Fenix中又引入了语义化标签,也类似以mybatis中的内置xml标签,语法使用上有所不同,可以一处定义,需要的地方复用。

前提:适用于 spring data jpa项目,jdk1.8及以上,spring data jpa版本必须在2.1.8.RELEASE及以上;而对于spring boot版本的要求2.1.5.RELEASE及以上。

二、特性

1、简单、灵活、轻量级、与springboot无缝集成和使用;

2、Fenix API只做增强和扩展,保持spring data jpa原有功能和特性;

3、提供了支持xml、java链式、API、动态条件注解等四种方式动态编写SQL;

三、项目中集成Fenix API

Fenix集成方式:

1、spring boot项目直接引入fenix-spring-boot-starter 依赖(激活Fenix相关配置,在启动类中加上 @EnableFenix 注解);

<dependency>
    <groupId>com.blinkfox</groupId>
    <artifactId>fenix-spring-boot-starter</artifactId>
    <version>2.5.0</version>
</dependency>

SpringBoot激活Fenix配置:

package com.wanmi.fenix;

import com.blinkfox.fenix.EnableFenix;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author slp
 */
@EnableFenix
@SpringBootApplication
public class FenixDemoApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(FenixDemoApplication.class, args);
    }

}

2、非spring boot项目则引入fenix原生依赖(配置激活需要手动定义初始化类加载相关自动配置信息)

<dependency>
    <groupId>com.blinkfox</groupId>
    <artifactId>fenix</artifactId>
    <version>2.5.0</version>
</dependency>

非SpringBoot激活Fenix配置:

package com.wanmi.fenix.init;

import com.blinkfox.fenix.config.FenixConfigManager;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @Description
 * @Author slp
 * @Date 2021/10/10 16:13
 */
@Component
public class FenixInitConfig {
    
    

    @PostConstruct
    public void init() {
    
    
        // Fenix相关配置初始化
        FenixConfigManager.getInstance().initLoad();
    }

}

本文采用Fenix集成springboot方式讲解:

基于Fenix API的4种使用方式:
1.1、基于XML方式SQL语义化标签

在application.yml中配置Fenix相关数据信息如下:

spring:
  application:
    name: fenix-demo
  datasource:
    url: jdbc:mysql://106.14.xx.xx:3306/fenix?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  # 开启JPA批量执行性能优化  
  jpa:
    properties:
      hibernate:
        order_inserts: true
        order_updates: true
        jdbc:
          batch_size: 500
          batch_versioned_data: true
    show-sql: true
    hibernate:
      ddl-auto: update
server:
  port: 8099
fenix:
  # 当开启之后,对 XML 中的 SQL 会进行实时文件流的读取和解析,不需要重启服务.
  debug: true
  # 成功加载 Fenix 配置信息后,是否打印启动 banner,默认 true.
  print-banner: true
  # 当该值为 true 时,就打印 SQL 信息,否则不打印
  print-sql: true
  # 扫描 Fenix XML 文件的所在位置,默认是 fenix 目录及子目录,可配置多个值,用英文逗号隔开.
  xml-locations: fenix

在自定义FenixUserRepository接口中继承FenixJpaRepository<T, ID>和FenixJpaSpecificationExecutor两个接口,这两个接口在Fenix API中是继JPA基础上做了扩展,提供了更多API场景的支持,类似MP在Mybatis基础上做的扩展,在用Fenix API的过程中并不影响原来JPA的使用。

FenixUserRepository中定义一个方法queryMyUsers(),该方法的作用可根据不同条件查询及匹配结果。

package com.wanmi.fenix.repository;

import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.blinkfox.fenix.jpa.QueryFenix;
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
import com.wanmi.fenix.entity.FenixUser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.Param;

import java.util.List;

/**
 * FenixUser Repository层
 *
 * @author slp
 * @date 2021-10-12 19:54:56
 */
public interface FenixUserRepository extends FenixJpaRepository<FenixUser, Long>, FenixJpaSpecificationExecutor<FenixUser> {
    
    

    @QueryFenix
    Page<FenixUser> queryMyUsers(@Param("ids") List<Long> ids, @Param("user") FenixUser user, Pageable pageable);

}

新建一个FenixUserRepository.xml文件,实现XML方式的SQL语义化标签定义。

<?xml version="1.0" encoding="UTF-8" ?>
<fenixs namespace="com.wanmi.fenix.repository.FenixUserRepository">

    <fenix id="queryMyUsers">
        select
        u
        from
        FenixUser as u
        where
        <in field="u.id" value="ids" match="ids != empty"/>
        <andLike field="u.name" value="user.name" match="user.name != empty"/>
        <andLike field="u.phone" value="user.phone" match="user.phone != empty"/>
        <andBetween field="u.createTime" start="user.createTime" end="user.updateTime" match="(?user.createTime != empty) || (?user.updateTime != empty)"/>
    </fenix>
    
</fenixs>

上面的SQL标签片段是Fenix API中内置的语义化标签,都是比较通用、灵活,比较于mybatis并不需要在写一些什么的一堆逻辑控制代码,直接用Fenix API语义化标签替代了。Fenix API为了满足于各种场景,适用于复杂的业务,对于内置的语义化标签的限制可能再不适用于业务上的拓展,也不满足于现在的业务需求,Fenix API为了满足各种场景需求及业务需要,提供了可灵活构造的自定义SQL语义化标签。

1.2、自定义SQL语义化标签

举个例子:有这样一个需求,现在大部分用户登录App或网站都只能看到自己的数据信息和所拥有的相关操作权限,比如,各个等级VIP所享受的权益都不一样,这里就将VIP等级分为三级,普通VIP等级、高级VIP等级、超级VIP等级,而这些等级的用户有不同的权限,对于简单的实现可以直接在xml里面通过if、choose标签即可完成,但对于xml中代码的重复性、臃肿性是不可避免的,这里通过Fenix API的拓展自定义SQL语义化标签去实现。

那么根据用户数据等级权限的xml语义化标签可以设计成下面这样,你也可以根据自定义业务意义去命名。

<levelAuth field="" userId=""/>

<!-- 前面附带 AND 前缀的标签. -->
<andLevelAuth field="" userId=""/>

下面在FenixUserRepository.xml自定义标签处理器片段中,由于用户自定义的语义化标签需要程序去识别、读取和解析并拼接成SQL片段和标签处理器。前提,需要创建一个LevelAuthHandler处理器类去实现Fenix API提供的FenixHandler处理器接口,然后,重写buildSqlInfo()方法即可。

package com.wanmi.fenix.handler;

import com.blinkfox.fenix.bean.BuildSource;
import com.blinkfox.fenix.config.annotation.Tagger;
import com.blinkfox.fenix.consts.Const;
import com.blinkfox.fenix.consts.SymbolConst;
import com.blinkfox.fenix.core.FenixHandler;
import com.blinkfox.fenix.helper.ParseHelper;
import com.blinkfox.fenix.helper.XmlNodeHelper;
import com.wanmi.fenix.entity.FenixUser;
import com.wanmi.fenix.service.FenixUserService;
import com.wanmi.fenix.util.ApplicationContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Node;

import java.util.Map;

/**
 * @Description 自定义等级数据权限处理器
 * @Author slp
 * @Date 2021/10/13 18:59
 */
@Tagger(value = "levelAuth")
@Tagger(value = "andLevelAuth", prefix = " AND ")
@Slf4j
public class LevelAuthHandler implements FenixHandler {
    
    

    private static final String AUTH_CODE= "authCode";      // 用于权限区分的权限编码

    /**
     * 构建xml定义的SQL语义化标签和SQL参数
     *
     * @param source
     */
    @Override
    public void buildSqlInfo(BuildSource source) {
    
    
        // 从 source 参数中获取到 XML Node 节点、拼接 SQL 的 StringBuilder 对象和 params 参数.
        Node node = source.getNode();
        StringBuilder join = source.getSqlInfo().getJoin();
        Map<String, Object> params = source.getSqlInfo().getParams();

        // 获取 field 和 userId 属性的文本值,该方法会检测属性值是否为空,如果为空,会抛出异常.
        String fieldText = XmlNodeHelper.getAndCheckNodeText(node, "attribute::field");
        String userIdText = XmlNodeHelper.getAndCheckNodeText(node, "attribute::userId");

        // 根据上下文参数,解析出 userId 的真实值,我这里假设 userId 为 Long 类型,你可以根据实际情况转换成其他类型.
        Long userId = (Long) ParseHelper.parseExpressWithException(userIdText, source.getContext());
        // 这里获取用户可通过缓存、数据库、也可通过前端传过来
        FenixUser user = ApplicationContextUtil.getBean(FenixUserService.class).findById(userId);
        Integer level = user.getLevel();
        String authCode = user.getAuthCode();
        log.info("当前用户ID:{},等级:{},权限码:{}", userId, level, authCode);
        // 如果是超级管理员(level == 0)的用户,由于拥有所有权限,所以不用生成查询条件的 SQL 片段,直接返回即可.
        if (level == 0) {
    
    
            return;
        }

        // 如果该用户是超级会员等级(level == 3)用户,则生成 x.authCode LIKE :authCode,
        // 参数值为权限编码的前 2 位数的前缀匹配,因为前 2 位相同说明是同一个超级会员等级.
        if (user.getLevel() == 3) {
    
    
            join.append(source.getPrefix()).append(fieldText)
                    .append(SymbolConst.LIKE).append(Const.COLON).append(AUTH_CODE);
            params.put(AUTH_CODE, authCode.substring(0, 2) + "%");
        } else if (user.getLevel() == 2) {
    
    
            // 如果该用户是高级会员等级(level == 2)用户,则生成 x.authCode LIKE :authCode%,
            // 参数值为权限编码的前 4 位数的前缀匹配,因为前 4 位相同说明是同一个高级会员等级.
            join.append(source.getPrefix()).append(fieldText)
                    .append(SymbolConst.LIKE).append(Const.COLON).append(AUTH_CODE);
            params.put(AUTH_CODE, authCode.substring(0, 4) + "%");
        } else if (user.getLevel() == 1) {
    
    
            // 如果该用户是普通会员等级(level == 1)用户,则直接生成等值查询即可,x.authCode = :authCode.
            join.append(source.getPrefix()).append(fieldText)
                    .append(SymbolConst.EQUAL).append(Const.COLON).append(AUTH_CODE);
            params.put(AUTH_CODE, authCode);
        }
    }

}

由于上面在自定义等级数据权限处理器时,我们需要配置标签与处理器的映射关系,直接在处理器类中加上@Tagger注解,此注解可以重复定义,然后,value值就是xml对应的自定义标签名,也可自定义标签的前缀名,后面还需要做如下配置,处理器映射关系有了,但你还需要通知Fenix API用户自定义了有哪些处理器,程序需要去扫描这些注解和处理器类的映射关系,加载到内存中,在识别、解析标签、拼接SQL便于获取用户上下文信息。

所以,你需要在application.yml文件中,配置扫描你自定义的标签处理器的包路径或java文件路径。

fenix:
  # 扫描你自定义的 XML 标签处理器的位置,默认为空,可以是包路径,也可以是 Java 或 class 文件的全路径名.
  # 可以使用 yaml 集合的形式配置多个值,properties 文件则用英文逗号隔开.
  handler-locations:
    - com.wanmi.fenix.handler

通过上面自定义的语义化标签,下面进行测试自定义标签是否生效。

在FenixUserRepository.xml中定义如下自定义标签片段:

    <fenix id="queryMyUsersAuthByUserIdAndLevel">
        select
        u
        from
        FenixUser as u
        <where>
            <!-- 自定义标签 -->
            <levelAuth field="u.authCode" userId="user.id"/>
            <!-- 下面是构建条件查询. -->
            <andLike field="u.name" value="user.name" match="user.?name != empty"/>
        </where>
    </fenix>

上面的自定义标签查询的作用是通过用户ID,所归属的等级权限编码去获取不同等级下所拥有的权限不同的数据。

2.1、基于JPQL的Java API方式

通常在业务比较复杂的场景,需要自定义于Java API方式去动态的控制整个业务逻辑,在这里可以使用链式API去完成,Fenix API内置了很多常用的xml对应的SQL语义化标签,使用方式和mybatis相同,但Fenix的Java动态SQL的API更加强大。

首先,创建一个FenixUserProvider类,在该类中创建一个返回类型为SqlInfo的对象方法,这里以根据不同的条件获取用户数据,方法参数需要与FenixUserRepository接口中的当前方法保持一致,并配合使用@Param注解的使用,方法中的参数位置可以不同,参数可以减少,但参数不能增多,对于增多了参数也没意义(多余的参数也会以null标识)。

package com.wanmi.fenix.provider;

import com.blinkfox.fenix.bean.SqlInfo;
import com.blinkfox.fenix.core.Fenix;
import com.blinkfox.fenix.helper.CollectionHelper;
import com.blinkfox.fenix.helper.StringHelper;
import com.wanmi.fenix.entity.FenixUser;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;

/**
 * @Description
 * @Author slp
 * @Date 2021/10/14 23:22
 */
public class FenixUserProvider {
    
    

    /**
     * 通过 Java API 来拼接得到 {@link SqlInfo} 的方式来查询用户信息.
     *
     * @param userIds 用户 ID 集合
     * @param fenixUser 用户信息
     * @param startTime 开始时间
     * @param endTime 结束时间
     * @return {@link SqlInfo}
     */
    public SqlInfo findFenixUserByCondition(@Param("userIds") String[] userIds, @Param("user") FenixUser fenixUser,
                                      @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime) {
    
    
        return Fenix.start()
                .select("u")
                .from("FenixUser").as("u")
                .where()
                // field ---> value ---> match
                .in("u.id", userIds, CollectionHelper.isNotEmpty(userIds))
                .andLike("u.name", fenixUser.getName(), StringHelper.isNotBlank(fenixUser.getName()))
                .andLike("u.level", fenixUser.getLevel(), StringHelper.isNotBlank(fenixUser.getLevel().toString()))
                .andBetween("u.createTime", startTime, endTime, startTime != null || endTime != null)
                .end();
    }
}

在FenixUserRepository接口中添加一个findFenixUserByCondition()方法,这里在方法上使用了@QueryFenix注解,在注解元素里面主要使用了provider、method两个属性,在使用Java API动态链式时,method方法可不设置,但要保持与provider类里面的方法名一致,因为Fenix API会默认匹配在接口中定义的方法名为targer对象,provider属性在此方式下是必填项,需要填写你定义的提供者类。

@QueryFenix(provider = FenixUserProvider.class, method = "findFenixUserByCondition")
    Page<FenixUser> findFenixUserByCondition(@Param("userIds") String[] userIds, @Param("user") FenixUser fenixUser,
                                            @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
3.1、基于Specification的Java API方式

在Fenix中编写动态SQL去满足各种业务场景中,也可以使用spring data jpa中提供的Specification方式去实现,而在Fenix中对该种方式的动态查询能力进一步的做了链式封装,更满足现状业务的场景。

在FenixUserRepository接口中,继承FenixJpaSpecificationExecutor接口即可,Fenix API提供的该接口在JPA中 对JpaSpecificationExecutor进一步封装,并不影响正常JPA上的使用,但在该接口中提供了更多的默认接口方法,可不用写 FenixSpecification.of()方法的中间层,可直接通过调用Fenix API提供的方法灵活调用。

public interface FenixUserRepository extends FenixJpaRepository<FenixUser, Long>, FenixJpaSpecificationExecutor<FenixUser> {
    
    
}

在业务层可以直接通过该接口调用提供的所需方法,不需要定义额外的查询方法,也不需要写JPQL(或SQL)语句,更为直接方便的调用。

在下面通过在业务层中,直接调用findAll()方法做演示:

/**
	 * 根据多条件获取用户信息
	 * @param ids
	 * @param name
	 * @param phone
	 * @param startTime
	 * @param endTime
	 * @return
	 */
	public List<FenixUser> findAll(Long[] ids, String name, String phone, String startTime, String endTime) {
    
    
		return fenixUserRepository.findAll(build -> build.andIn("id", ids, ids != null && ids.length > 0)
				.andLike("name", name, name != null).andLike("phone", phone,
						phone != null).andBetween("createTime", startTime, endTime,
						startTime != null && endTime != null).build());
	}

对应上面的使用,Fenix API还提供了更多的动态查询条件API方法,为满足于各种条件下业务的需要。

4.1、基于Specification的Bean注解方式

在Fenix中基于Specification方式编写动态SQL,也可采用JavaBean的去实现,以及将JavaBean的属性上加上匹配条件注解,对应这些注解Fenix API提供了很多比较常用的,想要进一步了解,可以去研究对应的源码,也可自定义条件注解,去实现业务场景中的各种复杂条件逻辑操作。

在JavaBean形式的条件下,根据每个属性上的条件注解就可完成具体的业务逻辑操作,透明的避免了内部的实现复杂逻辑。

首先,创建一个FenixUserRequest请求体,在当前body里面可定义多个属性条件查询,根据业务需求在对应的属性上加上条件匹配注解即可,然后,在对应的业务层使用的时候,直接通过当前参数对象完成SQL操作(也可前端入参)。

package com.wanmi.fenix.req;

import com.blinkfox.fenix.specification.annotation.Between;
import com.blinkfox.fenix.specification.annotation.In;
import com.blinkfox.fenix.specification.annotation.Like;
import com.blinkfox.fenix.specification.handler.bean.BetweenValue;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

/**
 * @Description 自定义JavaBean class 请求条件注解
 * @Author slp
 * @Date 2021/10/15 12:52
 */
@Data
@EqualsAndHashCode
public class FenixUserRequest implements Serializable {
    
    

    /**
     * 根据多个用户id查询,可以是数组或Collection集合,在value中需要指定持久性实体的属性名
     */
    @In(value = "id")
    private List<Long> ids;

    /**
     * 根据名称实现模糊查询
     */
    @Like
    private String name;

    /**
     * 根据手机号实现模糊查询
     */
    @Like
    private String phone;

    /**
     * 在一定时间范围内的注册用户查询
     */
    @Between
    private BetweenValue<Date> createTime;

}

在业务层直接调用Fenix API提供的持久层方法传入JavaBean条件注解,即可完成动态条件逻辑实现,配合Specification方式,无需定义额外的JPQL(或SQL)语句,下面通过单元测试演示:

前提,在匹配JavaBean对象的属性是否需要生成对应的条件,默认以是否为空做为标识去判断,具体判断是否为空如是否满足以下条件之一。

1)、 普通 Java 对象是 null

2)、字符串是否为“空字符串”,即 isBlank 的逻辑;

3)、 数组或集合是否为 null 或内容为空。

一个对象是否为空满足上面的条件之一,JavaBean里面的属性条件注解就不生效,而在定义基本类型属性作为条件注解时,是有默认值的,也就是说该项属性一直都会作为条件去查询,建议在定义JavaBean的属性值作为条件注解时,采用包装类型去定义属性条件。

Fenix API的JavaBean作为条件注解有一个特点,就是对属性的默认识别机制是否为null而设计的。

若想要控制JavaBean的属性条件注解是否生效,可以在当前JavaBean中定义一个与匹配属性值一样的无参方法,返回类型为Boolean即可(具体实现比较简单,不作为讲解)。

4.2、自定义条件注解实现

比如,有时候在使用Fenix API的过程中,内置JavaBean条件注解不满足当前业务场景和业务需求,这样你就会想到是否可以自定义条件注解去实现,在自定义条件注解配合条件处理器映射关系即可达到这样的目的。

如想根据JavaBean条件注解查询该字段不为空的对象,我们可以创建一个CustomerIsNotNull自定义条件注解,在该注解里面定义一个value值对应标识实体字段名,想要自定义条件注解生效,还需要在创建一个映射条件处理器。

需要创建CustomerIsNotNullPredicateHandler处理器类,继承Fenix API提供的AbstractPredicateHandler类,并重写getAnnotation()、buildPredicate()方法即可。

package com.wanmi.fenix.handler;

import com.blinkfox.fenix.specification.handler.AbstractPredicateHandler;
import com.wanmi.fenix.anno.CustomerIsNotNull;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import java.lang.annotation.Annotation;

/**
 * @Description 自定义字段不为空条件注解处理器
 * @Author slp
 * @Date 2021/10/15 14:20
 */
public class CustomerIsNotNullPredicateHandler extends AbstractPredicateHandler {
    
    

    /**
     * 返回自定义的条件注解
     * @return
     */
    @Override
    public Class<? extends Annotation> getAnnotation() {
    
    
        return CustomerIsNotNull.class;
    }

    /**
     * 处理自定义条件注解,实现字段不为空的逻辑处理
     * @param criteriaBuilder   条件构建对象
     * @param from  source对象
     * @param fieldName 字段名
     * @param value 字段值
     * @param annotation  自定义条件注解
     * @param <Z>   泛型下限
     * @param <X>   泛型上限
     * @return
     */
    @Override
    public <Z, X> Predicate buildPredicate(CriteriaBuilder criteriaBuilder, From<Z, X> from, String fieldName,
                                           Object value, Annotation annotation) {
    
    
        return criteriaBuilder.and(super.buildIsNotNullPredicate(criteriaBuilder, from, value));
    }
}

自定义条件注解处理器定义好后,需要将处理器初始化,加载到内存中,直接将处理器的扫描配置在yaml文件中即可。

通过上面的自定义条件处理器,在JavaBean中的属性加上该注解便可完成具体业务条件字段的构造和实现。

增强版的批量增删改(更高效的批量操作)

目前,在JPA中自带的优化配置中,这些批量操作的相关优化配置项默认未开启,需要在yaml中或properties文件中去开启这些配置项,但JPA中提供的saveAll()批量保存或更新效率很低,其实,通过源码分析可发现saveAll()方法的实现是在循环迭代调用save()方法,并且此方法是先通过查询当前持久对象是否存在,然后,才去新增或更新操作,也就是执行一次save()方法有两次SQL的操作,毕竟,对于大数量来说,肯定会吃不消的,可能会造成并发方面的问题;因此,Fenix API在JPA的基础上提供了增强版的批量新增和更新API方法,对大批量数据,执行时间耗时短,执行效率高。

1.1、开启JPA优化配置:
spring:
  jpa:
    # 以下配置主要用于提高 jpa 批量操作的相关性能.
    properties:
      hibernate:
        order_inserts: true
        order_updates: true
        jdbc:
          batch_size: 500
          batch_versioned_data: true
1.2、Fenix增强版批量操作相关API方法

Fenix API在版本2.4.0提供了比saveAll()更高效的批量操作的一系列方法,但Fenix中并没有提供类似JPA中的saveAll()可操作批量新增又可批量更新实现的方法,为了实现提高批量执行效率,Fenix将批量操作的方法都单独做了具体的实现和优化。

如需使用这些方法,只需在持久层FenixUserRepository接口中继承FenixJpaRepository<T,ID>接口即可。

在Fenix API中增强版批量新增和更新的方法提供了两个重载方法:

	// 仅能用于新增数据的场景
    <S extends T> void saveBatch(Iterable<S> entities);

	// 仅能用于新增数据的场景
    <S extends T> void saveBatch(Iterable<S> entities, int batchSize);

	// 仅能用于更新数据的场景
    <S extends T> void updateBatch(Iterable<S> entities);

   	// 仅能用于更新数据的场景
    <S extends T> void updateBatch(Iterable<S> entities, int batchSize);

而对于JPA中批量删除都是基于实体集合去删除的,但通常你的业务场景需要根据ID集合去做批量删除数据,Fenix API在JPA基础上增加了根据ID集合批量删除,提供了如下批量删除方法。

	// 此批量删除方法效率相对较低,底层是循环调用delete()方法
    void deleteByIds(Iterable<ID> ids);

   	// 批量删除效率高,采用in方式,只执行一次
    void deleteBatchByIds(Iterable<ID> ids);

    // 可配置批量删除数据量,效率高,只执行一次
    void deleteBatchByIds(Iterable<ID> ids, int batchSize);

针对Fenix API提供的批量操作可满足于大部分数据量大的场景,减少了对数据库之间的交互,提高了性能、优化了效率。

在百万数据量同时批量新增总耗时10min,是比较客观的,Fenix API在批量方法实现中,是JPA基础上每次执行上减少了一次磁盘IO交互,但对于亿级别数据量,可采用分批次去处理(同步或异步刷盘),在新增时可一次批量插入所有数据,这样就很大程度上减少不必要的IO操作。

2.1、针对业务场景需求只更新非空字段属性的增量更新

在实际业务场景中,更新数据时,比如,前端传过来的对象数据有的字段值为空,也有的不为空,此时,我就只想更新传过来参数字段值不为空的数据,而不是把空值也一起覆盖保存到数据库中,否则,就会造成数据库中有些数据字段值丢失的情况,为了避免上述这种问题,Fenix API提供了相对应的方法即可实现。

Fenix API提供了两个增量更新字段属性值的方法。

	// 新增或更新实体类中非 null 属性的字段值.
    <S extends T> S saveOrUpdateByNotNullProperties(S entity);

  	// 批量新增或更新实体类中非 null 属性的字段值.
    <S extends T> void saveOrUpdateAllByNotNullProperties(Iterable<S> entities);

注意:上面的两个增量更新的方法,执行会根据ID去查询数据库是否存在当前对象,若存在,则更新,否则执行新增操作;批量增量更新内部实现是基于循环调用saveOrUpdateByNotNullProperties()方法,对于数据量特别大,建议可分批次去执行,因为会影响性能问题,相当于增量更新一次,需要有两次IO的产生,可根据业务场景去使用,Fenix API本身是基于各种业务场景去实现的,总体来说能够满足业务才是最重要的。

增加多种主键ID生成策略

Fenix API在2.4.0版本中,增加了三种主键ID生成策略,同时也满足在Java API中去调用生成对应配置主键策略生成的ID。

1)、雪花算法ID(类型:Long);

2)、64位雪花算法ID(类型:String);

3)、64为UUID(类型:String).

在JPA中要使用自定义ID生成策略,则需要在实体对象的属性ID字段上加上@GeneratedValue注解中的generator属性值要与@GenericGenerator注解中的name属性值对应,strategy属性值可配置对应的生成策略,可配置下面Fenix API提供的生成策略即可。

雪花算法生成策略:com.blinkfox.fenix.id.SnowflakeIdGenerator;

62位雪花生成策略:com.blinkfox.fenix.id.Snowflake62RadixIdGenerator;

64位UUID类型生成策略: Uuid62RadixIdGenerator 。

通常在业务中使用可通过Java API方式调用生成对应策略算法的ID即可。

四、总结

1、Fenix API提供了更多的API方法满足于各种业务场景;

2、可灵活的自定义条件注解去解决复杂的SQL查询,屏蔽了内部实现细节;

3、提供了可自定义SQL语义化标签去实现复杂业务的控制及处理;

4、在JPA基础上增加了更加强大的批量操作API方法,性能上是有更好的选择;

5、提供了增量更新API方法,对于非空字段的更新。

猜你喜欢

转载自blog.csdn.net/weixin_43322048/article/details/121058436