红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

JPA 学习笔记 6:Fenix

2025年10月8日 137点热度 1人点赞 0条评论

快速开始

需要先创建 Spring Boot 项目并整合 Spring Data JPA,可以参考这里。

添加 Fenix 依赖:

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

按需添加 fenix 配置:

fenix:
    # 对 XML 中的 SQL 动态读取,修改 XML 后无需重启服务,生产环境需要关闭
    debug: true
    # 启动时打印 banner
    print-banner: true
    # 是否打印 Fenix 生成的 SQL 信息
    print-sql: true
    # 扫描 Fenix XML 文件的所在位置,默认是 fenix 目录及子目录,可以用 yaml 文件方式配置多个值.
    xml-locations: fenix

添加实体类:

@Table(name = "tb_blog")
@Entity
@Data
public class Blog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(length = 20)
    private String title;
    private String content;
    @Column(length = 10)
    private String author;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

添加存储库接口:

public interface BlogRepository extends JpaRepository<Blog, Long> {
    @QueryFenix("BlogRepository.queryBlogs")
    Page< Blog> queryBlogs(Blog blog, Pageable pageable);
}

接口方法上的@QueryFenix("BlogRepository.queryBlogs")注解用于定位 XML 文件中的 JPQL 语句。

建议安装 Idea 插件 Fenix。

添加 XML src/main/resources/fenix/BlogRepository.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<fenixs namespace="BlogRepository">
    <fenix id="queryBlogs">
        select b
        from Blog as b
        where
        1=1
        <andLike field="b.title" value="blog.title" match="blog.title!=empty"/>
        <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
        <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime" match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
    </fenix>
</fenixs>

fenix标签中可以定义 JPQL 语句。

测试用例:

final int PAGE_NUM = 1;
final int PAGE_SIZE = 5;
Pageable pageable = PageRequest.of(PAGE_NUM - 1, PAGE_SIZE);
Blog blog = new Blog();
blog.setTitle("NuS");
Page<Blog> blogPage = blogRepository.queryBlogs(blog, pageable);
System.out.println("总行数:" + blogPage.getTotalElements());
System.out.println("总页数:" + blogPage.getTotalPages());
System.out.println("当前页数据:" + blogPage.getContent());
System.out.println("当前页:" + (blogPage.getNumber() + 1));
System.out.println("每页大小:" + blogPage.getSize());
System.out.println("是否有下一页:" + blogPage.hasNext());
System.out.println("是否有上一页:" + blogPage.hasPrevious());

provider

可以使用@QueryFenix为方法指定一个 provider 类以实现具体的查询逻辑:

@QueryFenix(provider = BlogSqlProvider.class)
List<Blog> queryBlogsWithProvider(@Param("blog") Blog blog);

provider 类:

public class BlogSqlProvider {
    public SqlInfo queryBlogsWithProvider(@Param("blog") Blog blog){
        return Fenix.start()
                .select("b")
                .from("Blog").as("b")
                .where("1=1")
                .andLike("b.title", blog.getTitle(), StringHelper.isNotBlank(blog.getTitle()))
                .andLike("b.author", blog.getAuthor(), StringHelper.isNotBlank(blog.getAuthor()))
                .andBetween("b.createTime", blog.getCreateTime(), blog.getUpdateTime())
                .end();
    }
}

provider 类中的方法名和形参列表要与存储库接口方法一致,返回的是 SqlInfo 类型。

FenixJpaSpecificationExecutor

Spring Data JPA 的JpaSpecificationExecutor同样可以在使用 Fenix 时用于以编程方式实现 JPQL,不过 Fenix 封装了一个子接口FenixJpaSpecificationExecutor,提供更方便的写法:

public interface BlogRepository extends JpaRepository<Blog, Long>, FenixJpaSpecificationExecutor<Blog> {

示例:

final int PAGE_NUM = 1;
final int PAGE_SIZE = 5;
Blog blog = new Blog();
blog.setTitle("NuS");
Pageable pageable = PageRequest.of(PAGE_NUM - 1, PAGE_SIZE);
FenixPredicate specification = (builder) -> {
    return builder
        .andLike("title", blog.getTitle())
        .andLike("author", blog.getAuthor(), StringHelper.isNotBlank(blog.getAuthor()))
        .andBetween("createTime", blog.getCreateTime(), blog.getUpdateTime(), blog.getCreateTime() != null || blog.getUpdateTime() != null)
        .build();
};
Page<Blog> blogPage = blogRepository.findAll(specification, pageable);

Bean

可以定义一个 Bean 用于传输查询参数:

@Data
public class BlogParam {
    @Like
    private String title;
    @Like
    private String author;
    @Between("createTime")
    private BetweenValue<Date> createTime;
}

可以使用FenixJpaSpecificationExecutor接口中的方法利用 Bean 完成查询:

final int PAGE_NUM = 1;
final int PAGE_SIZE = 5;
BlogParam blogParam = new BlogParam();
blogParam.setTitle("NuS");
Pageable pageable = PageRequest.of(PAGE_NUM - 1, PAGE_SIZE);
Page<Blog> blogPage = blogRepository.findAllOfBean(blogParam, pageable);

@QueryFenix

XML 的fenixs标签的名空间可以命名为对应的存储库接口的全名:

<fenixs namespace="cn.icexmoon.demo.repository.BlogRepository">

此时在存储库接口的方法的@QueryFenix注解中不需要再指定名空间:

@QueryFenix("queryBlogs")
Page<Blog> queryBlogs(Blog blog, Pageable pageable);

如果 XML 的fenix标签的id与接口方法名相同:

<fenix id="queryBlogs">

标签 ID 也可以省略:

@QueryFenix
Page<Blog> queryBlogs(Blog blog, Pageable pageable);

如果存储库接口方法设置了Pageable类型的参数,Fenix 会自动添加一条 SELECT 语句用于查询数据总数进行分页。如果结果不符合预期,可以自行编写获取数据总数的 JPQL 或 SQL:

@QueryFenix(countQuery = "queryBlogsTotal")
Page<Blog> queryBlogs(@Param("blog") Blog blog, Pageable pageable);

对应的 JPQL 片段:

<fenix id="queryBlogsTotal">
    select count(*)
    from Blog as b
    where
    1=1
    <andLike field="b.title" value="blog.title" match="blog.title!=empty"/>
    <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
    <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime" match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
</fenix>

MVEL 模版

Fenix 的 XML 文件支持 MVEL 模版语法:

<fenix id="queryBlogs">
    select b
    from Blog as b
    where
    1=1
    @if{?blog.title != empty}
    AND b.title like concat('%',#{blog.title},'%')
    @end{}
    @if{?blog.author != empty}
    AND b.author like concat('%',#{blog.author},'%')
    @end{}
    @if{(?blog.createTime != empty) || (?blog.updateTime != empty)}
    AND (b.createTime between #{blog.createTime} and #{blog.updateTime})
    @end{}
</fenix>

这里的#{}是 Fenix 的差值语法,会生成对应的 JPQL 具名参数,可以有效防止 SQL 注入。

多层 @if 嵌套也是可以的:

        <fenix id="queryEmployees">
            select e
            from Employee as e
            where
            1=1
            @if{employee.name!=empty || employee.email!=empty}
                @if{employee.name!=empty}
                    and e.name like #{employee.name}
                @end{}
                @if{employee.email!=empty}
                    and e.email like #{employee.email}
                @end{}
            @else{}
                <!-- 如果检索条件不包含用户名和邮箱,不返回任何数据 -->
                and 1=0
            @end{}
        </fenix>

需要注意缩进格式。

更多 MVEL 模版写法可以参考这里。

SQL 标签

Fenix 提供一系列用于构造 JPQL 的标签。

equal

<equal>标签用于生成包含=条件的 JPQL 语句。

示例:

<andEqual field="b.title" value="blog.title" match="?blog.title!=empty"/>

生成的 JPQL:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b where 1=1 AND b.title = :blog_title
----- Params: {blog_title=NuS}

与equal相关的标签还有:

标签 JPQL
notEqual field != :value
andNotEqual and field != :value
orNotEqual or field != :value
greaterThan field > :value
andGreaterThan and field > :value
orGreaterThan or field > :value
lessThan field < :value
andLessThan and field < :value
orLessThan or field < :value
greaterThanEqual field >= :value
andGreaterThanEqual and field >= :value
orGreaterThanEqual or field >= :value
lessThanEqual field <= :value
andLessThanEqual and field <= :value
orLessThanEqual or field <= :value

like

<andLike field="b.title" value="blog.title" match="blog.title!=empty"/>

生成的 JPQL 如下:

select b from Blog as b where 1=1 AND b.title LIKE :blog_title

参数:

{blog_title=%NuS%}

如果不需要%?%的匹配方式,比如只需要右模糊匹配,可以:

<andLike field="b.title" pattern="@{blog.title}%" match="blog.title!=empty"/>

需要注意的是,这个示例有问题,@{blog.title}会直接用差值语法插入内容到 JPQL,而非#{...}的具名参数 JPQL,所以这里有潜在的 SQL 注入风险,正确的方式是使用标签<startsWith>。

startsWith

这个标签是like的变种,专门用于右模糊匹配,类似的还有endsWith。

示例:

<andStartsWith field="b.title" value="blog.title" match="blog.title!=empty"/>

生成的 JPQL 和参数:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b where 1=1 AND b.title LIKE :blog_title
----- Params: {blog_title=NuS%}

Fenix 解析 XML 后自动生成了右侧带%的参数。

endsWith

用途和用法见startsWith。

between

用于生成判断是否在某个时间段内的 JPQL 条件语句:

<andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime" match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>

生成的 JPQL:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b where 1=1 AND b.createTime BETWEEN :blog_createTime AND :blog_updateTime
----- Params: {blog_updateTime=2025-08-31 23:59:59, blog_createTime=2025-07-01 00:00:00}

需要注意的是,开始时间对应的属性start和结束时间对应的end不能搞混,开始时间一定要小于结束时间,否则就查询不出结果,这点和 SQL 的语法是一致的:

<!-- 错误的写法,查询不到结果 -->
<andBetween field="b.createTime" end="blog.createTime" start="blog.updateTime" match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>

时间段一端为null,只有另一端有有效的时间值是被允许的,此时 Fenix 会正确处理并生成 JPQL:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b where 1=1 AND b.createTime <= :blog_updateTime
----- Params: {blog_updateTime=2025-08-31 23:59:59}

这也是为什么between标签的match属性通常写为(?blog.createTime != empty) || (?blog.updateTime != empty)。

in

用于生成使用in作为条件语句的 JPQL:

<andIn field="b.id" value="ids" match="?ids!=empty"/>

生成的 JPQL:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b where 1=1 AND b.id IN :ids
----- Params: {ids=[1, 2, 3]}

isNull

在条件语句中判断字段是否为 NULL:

<andIsNotNull field="b.createTime"/>

生成的 JPQL:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b where 1=1 AND b.createTime IS NOT NULL
----- Params: {}

trimWhere

为了避免所有条件标签因为match为false导致的where之后产生空语句,进而导致报错,我们通常需要添加一个 1=1 这样的条件:

<fenix id="queryBlogs">
    select b
    from Blog as b
    where
    1=1
    <andIsNotNull field="b.createTime"/>
    <andEqual field="b.title" value="blog.title" match="?blog.title!=empty"/>
    <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
    <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime" match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
</fenix>

可以使用trimWhere标签简化这一点:

<fenix id="queryBlogs">
    select b
    from Blog as b
    <trimWhere>
        <andEqual field="b.title" value="blog.title" match="?blog.title!=empty"/>
        <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
        <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime"
                    match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
    </trimWhere>
</fenix>

这个标签可以正确处理各种情况下的 where 子句,包括在没有任何条件语句时不拼接where:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b
----- Params: {}

以及首个生效的条件标签是andXXX时自动去除and:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b WHERE b.title = :blog_title
----- Params: {blog_title=NuS}

trimWhere标签于v2.5.0版本生效。

text

可以使用text标签插入任意的 JPQL 片段:

<text match="(?blog.title!=empty) &amp;&amp; (?blog.author!=empty)">
    and b.title like concat('%',#{blog.title},'%') and b.author like concat('%',#{blog.author},'%')
</text>

只有在blog.title和blog.author都不为空时text中的 JPQL 片段才会生效。

可以通过text标签的value属性添加一个 Map,这个 Map 可以用于填充text标签内的 JPQL 片段中的具名参数:

<text value="['title':blog.title,'author':blog.author]" match="(?blog.title!=empty) &amp;&amp; (?blog.author!=empty)">
    and b.title like concat('%',:title,'%') and b.author like concat('%',:author,'%')
</text>

生成的 JPQL:

-- Fenix XML: cn.icexmoon.demo.repository.BlogRepository.queryBlogs
-------- SQL: select b from Blog as b WHERE b.title like concat('%',:title,'%') and b.author like concat('%',:author,'%')
----- Params: {author=DD, title=23}

需要注意的是,text标签内只能包含 JPQL 或 SQL,不能包含 SQL 标签。

import

使用import可以在 JPQL 中插入其他的 JPQL,以提高 JPQL 的复用性。

比如:

<fenix id="queryBlogs">
    select b
    from Blog as b
    <trimWhere>
        <andLike field="b.title" value="blog.title" match="blog.title!=empty"/>
        <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
        <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime"
                    match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
    </trimWhere>
</fenix>
<fenix id="queryBlogsTotal">
    select count(*)
    from Blog as b
    <trimWhere>
        <andLike field="b.title" value="blog.title" match="blog.title!=empty"/>
        <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
        <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime"
                    match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
    </trimWhere>
</fenix>

queryBlogsTotal为了实现queryBlogs查询分页而定义的,所有两者除了返回字段的差异,搜索条件完全相同。如果修改了一者的搜索条件,就要同步修改另外一者,否者会产生 BUG。使用import进行重构可以解决这个问题:

<?xml version="1.0" encoding="UTF-8" ?>
<fenixs namespace="cn.icexmoon.demo.repository.BlogRepository">
    <fenix id="queryBlogs">
        select b
        from Blog as b
        <import fenixId="whereFragment"/>
    </fenix>
    <fenix id="queryBlogsTotal">
        select count(*)
        from Blog as b
        <import fenixId="whereFragment"/>
    </fenix>
    <fenix id="whereFragment">
        <trimWhere>
            <andLike field="b.title" value="blog.title" match="blog.title!=empty"/>
            <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
            <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime"
                        match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
        </trimWhere>
    </fenix>
</fenixs>

使用import导入的并不局限于本 XML 中的 JPQL 片段,也可以导入外部(其它 XML)的 JPQL 片段:

<fenixs namespace="BlogRepositoryFragment">
    <fenix id="whereFragment">
        <trimWhere>
            <andLike field="b.title" value="blog.title" match="blog.title!=empty"/>
            <andLike field="b.author" value="blog.author" match="blog.author!=empty"/>
            <andBetween field="b.createTime" start="blog.createTime" end="blog.updateTime"
                        match="(?blog.createTime != empty) || (?blog.updateTime != empty)"/>
        </trimWhere>
    </fenix>
</fenixs>
<?xml version="1.0" encoding="UTF-8" ?>
<fenixs namespace="cn.icexmoon.demo.repository.BlogRepository">
    <fenix id="queryBlogs">
        select b
        from Blog as b
        <import namespace="BlogRepositoryFragment" fenixId="whereFragment"/>
    </fenix>
    <fenix id="queryBlogsTotal">
        select count(*)
        from Blog as b
        <import namespace="BlogRepositoryFragment" fenixId="whereFragment"/>
    </fenix>
</fenixs>

choose

使用choose标签可以实现类似if...elseif...else 的语法:

<fenix id="updateUser">
    update User as u
    set u.name = #{user.name}
    ,u.age = #{user.age}
    @if{user.ageRange!=empty}
    ,u.ageRange = #{user.ageRange}
    @else{}
    ,u.ageRange =
    <choose when="?user.age==empty" then="null"
            when2="?user.age>60" then2="'老年'"
            when3="?user.age>35" then3="'中年'"
            when4="?user.age>20" then4="'青年'"
            when5="?user.age>10" then5="'少年'"
            else="'幼年'"/>
    @end{}
    where id = #{user.id}
</fenix>

表字段ageRange表示年龄段,一定的年龄对应一定的年龄段。通常这种映射关系在 Service 或 Repository 中处理,借助choose标签,这里在 XML 的 JPQL 定义中进行处理。

set

对实体更新时仅更新属性不为 null 的字段是一个非常常见的功能需求,可以借助 set 标签实现这一点:

<fenix id="updateUserNotNull">
    update User
    <set field="age" value="user.age" match="?user.age!=empty"
         field2="age_range" value2="user.ageRange" match2="?user.ageRange!=empty"
         field3="name" value3="user.name" match3="?user.name!=empty"/>
    where id = #{user.id}
</fenix>

如果只是简单的根据 ID 更新非空字段,可以不使用set标签拼接,直接使用 FenixJpaRepository 接口提供的方法saveOrUpdateByNotNullProperties:

User user = new User();
user.setId(1L);
user.setName("Bruce");
userRepository.saveOrUpdateByNotNullProperties(user);

更多 Fenix 的 SQL 标签可以查看🌶️ SQL 语义化标签 - Fenix 文档。

自定义标签

可以定义自定义标签,并创建对应的标签处理器以生成 JPQL。

比如:

<fenixs namespace="cn.icexmoon.demo.repository.OrderRepository">
    <fenix id="pageAll">
        select o
        from Order as o
        where
            <regionAuth field="regionCode" userId="1L"/>
    </fenix>
</fenixs>

这里的自定义标签regionAuth的用途是根据用户权限(所属区域和区域级别)提供数据过滤。

需要在配置中添加标签处理器所属的包:

fenix:
    handler-locations: cn.icexmoon.demo.fenix.handler

定义标签处理器:

@Tagger(value = "regionAuth")
@Tagger(value = "andRegionAuth", prefix = "and ")
public class RegionAuthHandler implements FenixHandler {
    private static final String REGION_CODE_PARAM_NAME = "region_code";
​
    @Override
    public void buildSqlInfo(BuildSource source) {
        Node node = source.getNode();
        String fieldText = XmlNodeHelper.getAndCheckNodeText(node, "attribute::field");
        String userIdText = XmlNodeHelper.getAndCheckNodeText(node, "attribute::userId");
        Long userId = (Long) ParseHelper.parseExpressWithException(userIdText, source.getContext());
        // 获取当前登录用户的权限
        User currentUser = UserHolder.getCurrentUser();
        if (currentUser == null || !userId.equals(currentUser.getId())) {
            throw new RuntimeException("用户权限不足!");
        }
        String regionCode = currentUser.getRegionCode();
        if (StrUtil.isEmpty(regionCode)) {
            throw new RuntimeException("用户权限不足");
        }
        Integer level = currentUser.getLevel();
        StringBuilder join = source.getSqlInfo().getJoin();
        Map<String, Object> params = source.getSqlInfo().getParams();
        if (level == null || level <= 0 || level >= 4) {
            throw new RuntimeException("用户权限不足");
        }
        // 生成类似 and region_code like :region_code 这样的 JPQL
        if (level == 1) {
            join.append(source.getPrefix()).append(fieldText)
                    .append(SymbolConst.LIKE).append(Const.COLON).append(REGION_CODE_PARAM_NAME);
            params.put(REGION_CODE_PARAM_NAME, regionCode.substring(0, 2) + "%");
        }
        else if (level == 2){
            join.append(source.getPrefix()).append(fieldText)
                    .append(SymbolConst.LIKE).append(Const.COLON).append(REGION_CODE_PARAM_NAME);
            params.put(REGION_CODE_PARAM_NAME, regionCode.substring(0, 4) + "%");
        }
        else{
            join.append(source.getPrefix()).append(fieldText)
                    .append(SymbolConst.EQUAL).append(Const.COLON).append(REGION_CODE_PARAM_NAME);
            params.put(REGION_CODE_PARAM_NAME, regionCode);
        }
    }
}

在实际使用中发现 Fenix 的自定义标签存在一些问题,比如如果在自定义标签中使用插值表达式:

<regionAuth field="regionCode" userId="@{uid}"/>

无法通过ParseHelper.parseExpressWithException解析,会报错。此外自定义标签也无法在trimWhere标签中正常使用。

返回自定义实体

有时候查询结果不是直接映射到实体,比如原生 SQL 查询。此时就需要将查询到的结果集映射到自定义实体。

使用原生 SQL 查询若干字段:

<fenix id="getAll">
    select r.code as region_code,
    r.name as name
    from tb_region as r
</fenix>

为查询结果定义一个 Bean:

@Data
public class RegionDTO {
    private String regionCode;
    private String name;
}

在存储库方法上用@QueryFenix注解定义返回结果映射的实体类:

@QueryFenix(resultType = RegionDTO.class, nativeQuery = true, resultTransformer = UnderscoreTransformer.class)
List<RegionDTO> getAll();

这里resultType属性指定保存返回结果的实体类,必须要有对应返回结果字段的 setter/getter。nativeQuery属性表明这是一个原生 SQL 而非 JPQL。resultTransformer指定结果转换器,用于处理特殊情况,比如这里 SQL 查询结果字段是下划线分格,比如region_code,实体属性则是驼峰风格,所以需要使用UnderscoreTransformer这个转换器进行处理才能正确将结果集保存到实体中。

Fenix 提供这几种转换器:

  • FenixResultTransformer:基于查询结果列 as 别名与属性同名的方式来转换为自定义 Bean 对象。(默认,并兼容老版本)

  • UnderscoreTransformer:基于查询结果列下划线转小驼峰(lowerCamelCase)的方式来转换为自定义 Bean 对象。

  • PrefixUnderscoreTransformer:基于查询结果列下划线转小驼峰(lowerCamelCase)并去除一些字段固有前缀(如:c_、n_、dt_ 等)的方式来转换为自定义 Bean 对象。

  • ColumnAnnotationTransformer:基于查询结果列与 VO 属性中 @Column(name = "xxx") 注解 name 相等的方式来转换为自定义 Bean 对象。

更多的 ID 生成策略

通常我们会使用 MySQL 的自增主键作为实体 ID,特殊情况下也会使用其他策略,比如雪花算法或 UUID。

雪花算法

Fenix 支持使用雪花算法的方式生成实体 ID:

@Table(name = "tb_book")
@Entity
@Data
@NoArgsConstructor
public class Book {
    @Id
    @SnowflakeIdGenerator
    private Long id;
    @Column(length = 20)
    private String name;
​
    public Book(String name) {
        this.name = name;
    }
}

这里的@SnowflakeIdGenerator是一个自定义注解:

@IdGeneratorType(com.blinkfox.fenix.id.SnowflakeIdGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
public @interface SnowflakeIdGenerator {
}

如果是 Hibernate <= 6.5,可以使用下面的写法:

@Id
@GeneratedValue(generator = "snowflake")
@GenericGenerator(name = "snowflake", type = SnowflakeIdGenerator.class)
private Long id;

生成的结果是雪花算法产生的 Long 类型(16位10进制)的主键,雪花算法相比 UUID 的优点在于整体自增趋势,在批量插入时不会产生页分裂等问题,性能更好。

也可以使用字符串形式的雪花算法主键,此时只需要 9 位字符串即可保存:

@Table(name = "tb_book_category")
@Entity
@Data
@NoArgsConstructor
public class BookCategory {
    @Id
    @SnowflakeStrIdGenerator
    @Column(columnDefinition = "char(9)")
    private String id;
    @Column(length = 20)
    private String name;
​
    public BookCategory(String name) {
        this.name = name;
    }
}

自定义注解:

@IdGeneratorType(com.blinkfox.fenix.id.Snowflake62RadixIdGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
public @interface SnowflakeStrIdGenerator {
}

NanoId

相比 UUID,NanoId 生成的序列更短,且生成速度更快。

@Table(name = "tb_car")
@Entity
@Data
@NoArgsConstructor
public class Car {
    @Id
    @NanoIdGenerator
    @Column(columnDefinition = "char(21)")
    private String id;
    private String brand;
​
    public Car(String brand) {
        this.brand = brand;
    }
}

自定义注解:

@IdGeneratorType(com.blinkfox.fenix.id.NanoIdGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
public @interface NanoIdGenerator {
}

UUID

标准的 UUID 序列是16进制32位字符串,将其转换为62进制可以将字符串长度缩减为19位。

@Table(name = "tb_shop")
@Entity
@Data
@NoArgsConstructor
public class Shop {
    @Id
    @Column(columnDefinition = "char(19)")
    @UUIDGenerator
    private String id;
​
    @Column(length = 20)
    private String name;
​
    public Shop(String name) {
        this.name = name;
    }
}

自定义注解:

@IdGeneratorType(com.blinkfox.fenix.id.Uuid62RadixIdGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
public @interface UUIDGenerator {
}

自定义主键生成

有时候需要用自定义的方式实现主键生成策略,比如利用 Redis 生成一个分布式系统可用的自增主键。

先定义主键生成策略:

public class CustomerIdGeneratorType implements IdentifierGenerator, StandardGenerator {
    @Override
    public Object generate(SharedSessionContractImplementor session, Object object) {
        return IdWorker.get62RadixUuid();
    }
}
  • IdentifierGenerator和StandardGenerator是 Hibernate 的两个主键生成相关的接口。

  • 这里简单利用 Fenix 的主键生成 API 生成主键,具体实现可以按照自己的需要编写生成策略,比如利用 Redis 生成分布式 ID。

添加自定义注解:

@IdGeneratorType(CustomerIdGeneratorType.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
public @interface CustomerIdGenerator {
}

利用自定义注解生成主键:

@Table(name = "tb_shop")
@Entity
@Data
@NoArgsConstructor
public class Shop {
    @Id
    @Column(columnDefinition = "char(19)")
    @CustomerIdGenerator
    private String id;
    // ...
}

本文的完整示例可以从这里获取。

参考资料

  • Fenix 文档

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: fenix jpa
最后更新:2025年10月15日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号