需要先创建 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 后无需重启服务,生产环境需要关闭
debugtrue
# 启动时打印 banner
print-bannertrue
# 是否打印 Fenix 生成的 SQL 信息
print-sqltrue
# 扫描 Fenix XML 文件的所在位置,默认是 fenix 目录及子目录,可以用 yaml 文件方式配置多个值.
xml-locationsfenix
添加实体类:
(name = "tb_blog")
public class Blog {
(strategy = GenerationType.IDENTITY)
private Long id;
(length = 20)
private String title;
private String content;
(length = 10)
private String author;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
添加存储库接口:
public interface BlogRepository extends JpaRepository<Blog, Long> {
("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 类以实现具体的查询逻辑:
(provider = BlogSqlProvider.class)
List<Blog> queryBlogsWithProvider(("blog") Blog blog);
provider 类:
public class BlogSqlProvider {
public SqlInfo queryBlogsWithProvider(("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 用于传输查询参数:
public class BlogParam {
private String title;
private String author;
("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注解中不需要再指定名空间:
("queryBlogs")
Page<Blog> queryBlogs(Blog blog, Pageable pageable);
如果 XML 的fenix标签的id与接口方法名相同:
<fenix id="queryBlogs">
标签 ID 也可以省略:
Page<Blog> queryBlogs(Blog blog, Pageable pageable);
如果存储库接口方法设置了Pageable类型的参数,Fenix 会自动添加一条 SELECT 语句用于查询数据总数进行分页。如果结果不符合预期,可以自行编写获取数据总数的 JPQL 或 SQL:
(countQuery = "queryBlogsTotal")
Page<Blog> queryBlogs(("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 文件支持 语法:
<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) && (?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) && (?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进行重构可以解决这个问题:
<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>
<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 标签可以查看。
自定义标签
可以定义自定义标签,并创建对应的标签处理器以生成 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-locationscn.icexmoon.demo.fenix.handler
定义标签处理器:
(value = "regionAuth")
(value = "andRegionAuth", prefix = "and ")
public class RegionAuthHandler implements FenixHandler {
private static final String REGION_CODE_PARAM_NAME = "region_code";
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:
public class RegionDTO {
private String regionCode;
private String name;
}
在存储库方法上用@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:
(name = "tb_book")
public class Book {
private Long id;
(length = 20)
private String name;
public Book(String name) {
this.name = name;
}
}
这里的@SnowflakeIdGenerator是一个自定义注解:
(com.blinkfox.fenix.id.SnowflakeIdGenerator.class)
(RetentionPolicy.RUNTIME)
({METHOD, FIELD})
public @interface SnowflakeIdGenerator {
}
如果是 Hibernate <= 6.5,可以使用下面的写法:
(generator = "snowflake")
(name = "snowflake", type = SnowflakeIdGenerator.class)
private Long id;
生成的结果是雪花算法产生的 Long 类型(16位10进制)的主键,雪花算法相比 UUID 的优点在于整体自增趋势,在批量插入时不会产生页分裂等问题,性能更好。
也可以使用字符串形式的雪花算法主键,此时只需要 9 位字符串即可保存:
(name = "tb_book_category")
public class BookCategory {
(columnDefinition = "char(9)")
private String id;
(length = 20)
private String name;
public BookCategory(String name) {
this.name = name;
}
}
自定义注解:
(com.blinkfox.fenix.id.Snowflake62RadixIdGenerator.class)
(RetentionPolicy.RUNTIME)
({METHOD, FIELD})
public @interface SnowflakeStrIdGenerator {
}
NanoId
相比 UUID,NanoId 生成的序列更短,且生成速度更快。
(name = "tb_car")
public class Car {
(columnDefinition = "char(21)")
private String id;
private String brand;
public Car(String brand) {
this.brand = brand;
}
}
自定义注解:
(com.blinkfox.fenix.id.NanoIdGenerator.class)
(RetentionPolicy.RUNTIME)
({METHOD, FIELD})
public @interface NanoIdGenerator {
}
UUID
标准的 UUID 序列是16进制32位字符串,将其转换为62进制可以将字符串长度缩减为19位。
(name = "tb_shop")
public class Shop {
(columnDefinition = "char(19)")
private String id;
(length = 20)
private String name;
public Shop(String name) {
this.name = name;
}
}
自定义注解:
(com.blinkfox.fenix.id.Uuid62RadixIdGenerator.class)
(RetentionPolicy.RUNTIME)
({METHOD, FIELD})
public @interface UUIDGenerator {
}
自定义主键生成
有时候需要用自定义的方式实现主键生成策略,比如利用 Redis 生成一个分布式系统可用的自增主键。
先定义主键生成策略:
public class CustomerIdGeneratorType implements IdentifierGenerator, StandardGenerator {
public Object generate(SharedSessionContractImplementor session, Object object) {
return IdWorker.get62RadixUuid();
}
}
IdentifierGenerator和StandardGenerator是 Hibernate 的两个主键生成相关的接口。这里简单利用 Fenix 的主键生成 API 生成主键,具体实现可以按照自己的需要编写生成策略,比如。
添加自定义注解:
(CustomerIdGeneratorType.class)
(RetentionPolicy.RUNTIME)
({METHOD, FIELD})
public @interface CustomerIdGenerator {
}
利用自定义注解生成主键:
(name = "tb_shop")
public class Shop {
(columnDefinition = "char(19)")
private String id;
// ...
}
本文的完整示例可以从获取。
参考资料

文章评论