工作中会发现很多东西数据现在不多,未来也不会多,建表的成本开销就太大了,很多时候都会选择使用枚举来做,但是又有一个问题,不使用表的格式,未来增加相关映射关系的时候就会很麻烦,头都大了,假设有三个枚举,我们就要维护三个枚举,如果涉及到映射关系,那就需要再加。
个枚举,。在下次呢,。后面的人去改代码的时候怎么办,。一个一个加枚举吗?肯定不太现实。
无表映射(Table-less Mapping)反而能带来更优雅、高效的解决方案。
对于同一个业务而言,如果不想建表(未来几年都不会扩展的时候,但是映射关系经常变更)
我见过的几种方式
1:配置中心配置+代码映射
企业项目大多都是基于nacos配置中心来区分不同环境的如test,overseatest,pre,overseapre等等,如何仍然使用枚举的化就会涉及到每次要重新发版,如果使用配置中心,就只需求去配置中心增加相关映射关系并重新服务就ok了。成本要小很多。
关系很简单,只涉及一个类,一个yml
以用户行为标签表做映射配置
类写法
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 用户行为标签映射配置类
*/
@Component
@RefreshScope // 支持配置热更新
@ConfigurationProperties(prefix = "user-tag-mapping")
@Data
public class UserTagMappingConfig {
//以下是一些通用配置,也可以nacos配置,。通过@value,这里只演示list字段
/**
* 配置版本
*/
private String version;
/**
* 最后更新时间
*/
private String lastUpdated;
/**
* 默认标签(未匹配到时使用)
*/
private String defaultTag;
/**
* 是否启用缓存
*/
private Boolean enableCache;
/**
* 缓存持续时间(秒)
*/
private Integer cacheDuration;
/**
* 映射关系列表
*/
private List<TagMappingItem> mappings = new ArrayList<>();
/**
* 根据行为类型获取标签名称(单条匹配)
*
* @param actionType 行为类型
* @return 对应的标签名称,未找到返回默认标签
*/
public String getTagNameByActionType(String actionType) {
if (actionType == null || actionType.trim().isEmpty()) {
return defaultTag;
}
return mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.filter(item -> actionType.equals(item.getActionType()))
.findFirst() // 返回第一条匹配的记录
.map(TagMappingItem::getTagName)
.orElse(defaultTag);
}
/**
* 根据行为类型获取所有匹配的标签名称(多条匹配)
*
* @param actionType 行为类型
* @return 所有匹配的标签名称列表
*/
public List<String> getAllTagNamesByActionType(String actionType) {
if (actionType == null || actionType.trim().isEmpty()) {
return new ArrayList<>();
}
return mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.filter(item -> actionType.equals(item.getActionType()))
.map(TagMappingItem::getTagName)
.distinct()
.collect(Collectors.toList());
}
/**
* 根据行为类型获取优先级最高的标签
*
* @param actionType 行为类型
* @return 优先级最高的标签名称
*/
public String getHighestPriorityTag(String actionType) {
if (actionType == null || actionType.trim().isEmpty()) {
return defaultTag;
}
return mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.filter(item -> actionType.equals(item.getActionType()))
.max((a, b) -> Integer.compare(
Optional.ofNullable(a.getPriority()).orElse(0),
Optional.ofNullable(b.getPriority()).orElse(0)
))
.map(TagMappingItem::getTagName)
.orElse(defaultTag);
}
/**
* 根据行为类型和条件上下文获取标签
*
* @param actionType 行为类型
* @param context 条件上下文(用于SpEL表达式判断)
* @return 符合条件的标签名称
*/
public String getTagNameWithCondition(String actionType, Map<String, Object> context) {
if (actionType == null || actionType.trim().isEmpty()) {
return defaultTag;
}
// 这里可以集成SpEL表达式引擎来评估conditions字段
// 简化版:如果有conditions配置,需要context不为空
return mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.filter(item -> actionType.equals(item.getActionType()))
.filter(item -> evaluateCondition(item.getConditions(), context))
.findFirst()
.map(TagMappingItem::getTagName)
.orElse(defaultTag);
}
/**
* 获取所有行为类型与标签的映射关系
*
* @return Map<actionType, List<tagName>>
*/
public Map<String, List<String>> getAllMappings() {
return mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.collect(Collectors.groupingBy(
TagMappingItem::getActionType,
Collectors.mapping(TagMappingItem::getTagName, Collectors.toList())
));
}
/**
* 检查行为类型是否存在映射
*
* @param actionType 行为类型
* @return 是否存在映射
*/
public boolean hasMappingForAction(String actionType) {
if (actionType == null || actionType.trim().isEmpty()) {
return false;
}
return mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.anyMatch(item -> actionType.equals(item.getActionType()));
}
/**
* 获取配置统计信息
*/
public Map<String, Object> getStats() {
long totalMappings = mappings.size();
long enabledMappings = mappings.stream()
.filter(item -> item.getEnabled() != Boolean.FALSE)
.count();
long uniqueActionTypes = mappings.stream()
.map(TagMappingItem::getActionType)
.distinct()
.count();
return Map.of(
"version", version,
"totalMappings", totalMappings,
"enabledMappings", enabledMappings,
"uniqueActionTypes", uniqueActionTypes,
"defaultTag", defaultTag
);
}
/**
* 评估条件表达式(简化版)
* 实际项目中可以集成Spring SpEL或Groovy引擎
*/
private boolean evaluateCondition(String condition, Map<String, Object> context) {
if (condition == null || condition.trim().isEmpty()) {
return true; // 无条件限制
}
if (context == null || context.isEmpty()) {
return false; // 有条件但无上下文,无法评估
}
// 简化实现:实际项目中应使用表达式引擎
// 这里只是示例,实际需要解析condition字符串
try {
// 示例:假设condition是简单的属性判断
// 实际应该用SpEL: SpelExpressionParser.parseExpression(condition).getValue(context, Boolean.class)
return true;
} catch (Exception e) {
// 表达式解析失败,默认不匹配
return false;
}
}
//映射的内部类相关字段定义
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class TagMappingItem implements Serializable {
/**
* 行为类型 - 作为查询键
*/
private String actionType;
/**
* 对应的标签名称 - 主要查询目标
*/
private String tagName;
/**
* 优先级(数字越大优先级越高)
*/
private Integer priority;
/**
* 标签过期天数
*/
private Integer expirationDays;
/**
* 附加条件(SpEL表达式)
*/
private String conditions;
/**
* 子标签(可选)
*/
private String subTag;
/**
* 标签描述(可选)
*/
private String description;
/**
* 是否启用
*/
@Builder.Default
private Boolean enabled = true;
/**
* 创建时间
*/
private String createTime;
}
}
yml文件
java
# 用户行为标签映射配置
user-tag-mapping:
version: "1.0.2"
last-updated: "2024-01-15T14:30:00Z"
# 通用配置
default-tag: "新用户"
enable-cache: true
cache-duration: 300 # 秒
# 核心映射列表
mappings:
- action-type: "login"
tag-name: "活跃用户"
priority: 10
expiration-days: 30
conditions: "frequency > 3 && lastLoginDays < 7"
- action-type: "purchase"
tag-name: "消费用户"
priority: 20
expiration-days: 90
conditions: "totalAmount > 100"
sub-tag: "高价值用户"
- action-type: "purchase"
tag-name: "普通买家"
priority: 15
expiration-days: 60
conditions: "totalAmount > 0 && totalAmount <= 100"
- action-type: "comment"
tag-name: "内容贡献者"
priority: 5
expiration-days: 180
conditions: "commentCount > 5"
- action-type: "share"
tag-name: "传播达人"
priority: 8
expiration-days: 90
conditions: "shareCount > 10"
- action-type: "login"
tag-name: "忠实用户"
priority: 25
expiration-days: 365
conditions: "loginDays > 100 && continuousLoginDays > 30"
- action-type: "register"
tag-name: "新注册用户"
priority: 1
expiration-days: 7
conditions: "registerDays < 7"
- action-type: "refund"
tag-name: "售后用户"
priority: 30
expiration-days: 180
conditions: "refundCount > 0"
- action-type: "subscribe"
tag-name: "关注用户"
priority: 3
expiration-days: 30
conditions: "subscribeTime != null"
2:嵌套枚举(区域概念模型)
举个例子,比如在做州国省城的映射关系的时候,不是每个国家都有省的概念的,也不是都有州的概念的,而对于区县市完全就可以使用通用的字段,这里可以建表来做,字段就没有那么多了,直接将大层面的东西用嵌套枚举做,就可以了。
java
/**
* 全球行政区划枚举树
*/
@Getter
@NoArgsConstructor
public enum AdministrativeRegion {
// 根节点 → 大洲 → 国家
WORLD("世界",
ASIA("亚洲",
CHINA("中国",
BEIJING("北京市", "京",
DISTRICT("东城区"), DISTRICT("西城区"), DISTRICT("朝阳区")),
SHANGHAI("上海市", "沪",
DISTRICT("黄浦区"), DISTRICT("浦东新区"), DISTRICT("徐汇区")),
GUANGDONG("广东省", "粤",
GUANGZHOU("广州市"), SHENZHEN("深圳市"), DONGGUAN("东莞市"))
),
JAPAN("日本", "JP",
TOKYO("东京都"), OSAKA("大阪府"), KYOTO("京都府")
)
),
//根节点,大洲-国家
EUROPE("欧洲",
FRANCE("法国", "FR",
ILE_DE_FRANCE("法兰西岛大区",
PARIS("巴黎市"))
),
GERMANY("德国", "DE",
BAVARIA("巴伐利亚州",
MUNICH("慕尼黑市")),
BERLIN("柏林州",
BERLIN_CITY("柏林市"))
)
)
);
//需要的名字
private final String name;
//代号
private final String code;
//父子关系
private final AdministrativeRegion[] children;
// 反向引用
private AdministrativeRegion parent;
// 叶节点构造(城市)
AdministrativeRegion(String name) {
this(name, null);
}
AdministrativeRegion(String name, String code) {
this(name, code, new AdministrativeRegion[0]);
}
// 非叶节点构造(有子节点)
AdministrativeRegion(String name, AdministrativeRegion... children) {
this(name, null, children);
}
AdministrativeRegion(String name, String code, AdministrativeRegion... children) {
this.name = name;
this.code = code;
this.children = children;
// 建立反向父子关系
for (AdministrativeRegion child : children) {
child.parent = this;
}
}
/**
* 查找完整路径:中国/北京市/朝阳区
*/
public static List<AdministrativeRegion> findPath(String... names) {
if (names == null || names.length == 0) {
return Collections.emptyList();
}
List<AdministrativeRegion> path = new ArrayList<>();
AdministrativeRegion current = WORLD;
for (String name : names) {
Optional<AdministrativeRegion> found = Arrays.stream(current.children)
.filter(region -> region.name.equals(name))
.findFirst();
if (found.isPresent()) {
current = found.get();
path.add(current);
} else {
// 路径中断
break;
}
}
return path.size() == names.length ? path : Collections.emptyList();
}
/**
* 获取所有叶子节点(所有添加的最底层枚举)
*/
public List<AdministrativeRegion> getAllCities() {
List<AdministrativeRegion> cities = new ArrayList<>();
traverseForLeaves(this, cities);
return cities;
}
private void traverseForLeaves(AdministrativeRegion node,
List<AdministrativeRegion> result) {
if (node.children.length == 0) {
result.add(node);
} else {
for (AdministrativeRegion child : node.children) {
traverseForLeaves(child, result);
}
}
}
/**
* 获取兄弟节点(同一层级的所有节点)
*/
public List<AdministrativeRegion> getSiblings() {
if (parent == null) {
return Collections.emptyList(); // 根节点没有兄弟
}
return Arrays.asList(parent.children);
}
/**
* 根据code快速查找(如"京"找北京)
*/
public static Optional<AdministrativeRegion> findByCode(String code) {
return deepFind(WORLD, region -> code.equals(region.code));
}
//optinal保证安全
private static Optional<AdministrativeRegion> deepFind(
AdministrativeRegion start,
Predicate<AdministrativeRegion> predicate) {
if (predicate.test(start)) {
return Optional.of(start);
}
for (AdministrativeRegion child : start.children) {
Optional<AdministrativeRegion> found = deepFind(child, predicate);
if (found.isPresent()) {
return found;
}
}
return Optional.empty();
}
public String getFullPath() {
List<String> names = new ArrayList<>();
AdministrativeRegion current = this;
while (current != null && current != WORLD) {
names.add(0, current.name);
current = current.parent;
}
return String.join("/", names);
}
public boolean isLeaf() {
return children.length == 0;
}
public int getDepth() {
int depth = 0;
AdministrativeRegion current = this;
while (current != null && current != WORLD) {
depth++;
current = current.parent;
}
return depth;
}
// 内部枚举:区分节点类型
private static enum NodeType {
CONTINENT, COUNTRY, PROVINCE, CITY, DISTRICT
}
}
3:图状枚举(鉴权基本概念模型)
java
/**
*
* 用户角色与权限、用户间关系枚举
*/
@Getter
public enum SocialGraph {
// 节点定义(用户角色)
ADMIN("管理员", RoleLevel.SYSTEM),
MODERATOR("版主", RoleLevel.MANAGEMENT),
VIP_USER("VIP用户", RoleLevel.PRIVILEGED),
REGULAR_USER("普通用户", RoleLevel.NORMAL),
GUEST("游客", RoleLevel.RESTRICTED),
// 内容类型
PUBLIC_POST("公开帖子", ContentType.PUBLIC),
PRIVATE_MESSAGE("私信", ContentType.PRIVATE),
GROUP_CHAT("群聊消息", ContentType.GROUP),
COMMENT("评论", ContentType.PUBLIC),
// 权限动作
READ("读取", ActionType.VIEW),
WRITE("写入", ActionType.MODIFY),
DELETE("删除", ActionType.MODIFY),
SHARE("分享", ActionType.SOCIAL),
INVITE("邀请", ActionType.SOCIAL);
private final String displayName;
private final NodeType type;
SocialGraph(String displayName, NodeType type) {
this.displayName = displayName;
this.type = type;
}
// 邻接表:定义节点间关系
private static final Map<SocialGraph, Set<Edge>> GRAPH = new EnumMap<>(SocialGraph.class);
// 关系权重矩阵
private static final Map<Pair<SocialGraph, SocialGraph>, Integer> WEIGHTS = new HashMap<>();
//先初始化
static {
initGraph();
initWeights();
}
private static void initGraph() {
// 角色 → 可访问的内容类型
addEdge(ADMIN, PUBLIC_POST, 1);
addEdge(ADMIN, PRIVATE_MESSAGE, 1);
addEdge(ADMIN, GROUP_CHAT, 1);
addEdge(MODERATOR, PUBLIC_POST, 1);
addEdge(MODERATOR, COMMENT, 1);
addEdge(VIP_USER, PUBLIC_POST, 1);
// 部分权限
addEdge(VIP_USER, GROUP_CHAT, 0.5);
addEdge(REGULAR_USER, PUBLIC_POST, 1);
// 内容类型 → 允许的操作
addEdge(PUBLIC_POST, READ, 1);
addEdge(PUBLIC_POST, WRITE, 0.8);
addEdge(PUBLIC_POST, DELETE, 0.3);
addEdge(PUBLIC_POST, SHARE, 1);
addEdge(PRIVATE_MESSAGE, READ, 1);
addEdge(PRIVATE_MESSAGE, WRITE, 1);
addEdge(PRIVATE_MESSAGE, DELETE, 0.5);
// 操作 → 需要的最低角色
addInverseEdge(READ, GUEST);
// 游客也能读
addInverseEdge(WRITE, REGULAR_USER);
addInverseEdge(DELETE, MODERATOR);
addInverseEdge(SHARE, REGULAR_USER);
addInverseEdge(INVITE, VIP_USER);
}
private static void initWeights() {
// 定义关系强度(用于最短路径计算)
WEIGHTS.put(Pair.of(ADMIN, PUBLIC_POST), 1);
WEIGHTS.put(Pair.of(ADMIN, DELETE), 1);
WEIGHTS.put(Pair.of(GUEST, READ), 5);
// 游客读权限"距离远"
WEIGHTS.put(Pair.of(VIP_USER, INVITE), 2);
}
/**
* 检查权限:角色是否能对内容执行操作
* 查找路径:角色 → 内容类型 → 操作
*/
public static boolean hasPermission(SocialGraph role,
SocialGraph contentType,
SocialGraph action) {
// 使用BFS查找是否存在路径
return findPath(role, action,
// 必须经过内容类型节点
node -> node == contentType, 3
// 最大深度限制
).isPresent();
}
/**
* 查找最短权限路径
*/
public static Optional<List<SocialGraph>> findShortestPermissionPath(
SocialGraph from, SocialGraph to) {
// Dijkstra算法实现
Map<SocialGraph, Integer> distances = new EnumMap<>(SocialGraph.class);
Map<SocialGraph, SocialGraph> previous = new EnumMap<>(SocialGraph.class);
PriorityQueue<SocialGraph> queue = new PriorityQueue<>(
Comparator.comparingInt(node -> distances.getOrDefault(node, Integer.MAX_VALUE))
);
distances.put(from, 0);
queue.add(from);
while (!queue.isEmpty()) {
SocialGraph current = queue.poll();
if (current == to) break;
for (Edge edge : GRAPH.getOrDefault(current, Collections.emptySet())) {
int weight = WEIGHTS.getOrDefault(
Pair.of(current, edge.target),
edge.defaultWeight
);
int newDist = distances.get(current) + weight;
if (newDist < distances.getOrDefault(edge.target, Integer.MAX_VALUE)) {
distances.put(edge.target, newDist);
previous.put(edge.target, current);
queue.add(edge.target);
}
}
}
// 重建路径
if (!distances.containsKey(to)) {
return Optional.empty();
}
List<SocialGraph> path = new LinkedList<>();
for (SocialGraph node = to; node != null; node = previous.get(node)) {
path.add(0, node);
}
return Optional.of(path);
}
/**
* 获取角色的所有直接权限
*/
public static Set<SocialGraph> getDirectPermissions(SocialGraph role) {
return GRAPH.getOrDefault(role, Collections.emptySet()).stream()
.map(edge -> edge.target)
.collect(Collectors.toSet());
}
/**
* 查找需要特定权限的最小角色
*/
public static Optional<SocialGraph> findMinimalRoleForAction(SocialGraph action) {
return Arrays.stream(values())
.filter(node -> node.type == RoleLevel.NORMAL ||
node.type == RoleLevel.PRIVILEGED ||
node.type == RoleLevel.MANAGEMENT)
.filter(role -> hasPermission(role, PUBLIC_POST, action)) // 以公开帖子为例
.min(Comparator.comparingInt(Enum::ordinal));
}
private static void addEdge(SocialGraph from, SocialGraph to, double weight) {
GRAPH.computeIfAbsent(from, k -> new HashSet<>())
.add(new Edge(to, weight));
}
private static void addInverseEdge(SocialGraph from, SocialGraph to) {
addEdge(from, to, 1.0);
// 双向关系
addEdge(to, from, 1.0);
}
private static Optional<List<SocialGraph>> findPath(
SocialGraph start, SocialGraph end,
Predicate<SocialGraph> mustPass, int maxDepth) {
Deque<PathNode> stack = new ArrayDeque<>();
Set<SocialGraph> visited = new HashSet<>();
stack.push(new PathNode(start, Collections.emptyList()));
while (!stack.isEmpty()) {
PathNode current = stack.pop();
if (current.depth > maxDepth) continue;
if (!visited.add(current.node)) continue;
List<SocialGraph> newPath = new ArrayList<>(current.path);
newPath.add(current.node);
if (current.node == end &&
(mustPass == null || newPath.stream().anyMatch(mustPass))) {
return Optional.of(newPath);
}
for (Edge edge : GRAPH.getOrDefault(current.node, Collections.emptySet())) {
stack.push(new PathNode(edge.target, newPath, current.depth + 1));
}
}
return Optional.empty();
}
private enum RoleLevel { SYSTEM, MANAGEMENT, PRIVILEGED, NORMAL, RESTRICTED }
private enum ContentType { PUBLIC, PRIVATE, GROUP }
private enum ActionType { VIEW, MODIFY, SOCIAL }
private enum NodeType { ROLE, CONTENT, ACTION }
private static class Edge {
final SocialGraph target;
final double defaultWeight;
Edge(SocialGraph target, double defaultWeight) {
this.target = target;
this.defaultWeight = (int) (defaultWeight * 10); // 转换为整数权重
}
}
private static class PathNode {
final SocialGraph node;
final List<SocialGraph> path;
final int depth;
PathNode(SocialGraph node, List<SocialGraph> path) {
this(node, path, 0);
}
PathNode(SocialGraph node, List<SocialGraph> path, int depth) {
this.node = node;
this.path = path;
this.depth = depth;
}
}
// 简单Pair实现
private static class Pair<K, V> {
final K key;
final V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
static <K, V> Pair<K, V> of(K key, V value) {
return new Pair<>(key, value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Pair)) return false;
Pair<?, ?> pair = (Pair<?, ?>) o;
return Objects.equals(key, pair.key) &&
Objects.equals(value, pair.value);
}
@Override
public int hashCode() {
return Objects.hash(key, value);
}
}
public String getDisplayName() { return displayName; }
public NodeType getNodeType() { return type; }
public static Set<SocialGraph> getAllRoles() {
return Arrays.stream(values())
.filter(node -> node.type == NodeType.ROLE)
.collect(Collectors.toSet());
}
public static Set<SocialGraph> getAllActions() {
return Arrays.stream(values())
.filter(node -> node.type == NodeType.ACTION)
.collect(Collectors.toSet());
}
}