约定
约定是团队的默契,为我们的工作提供了一致性和可预测性。统一的规则和准则确保了代码的清晰、高效和无冲突。因此,约定在团队合作和项目开发中至关重要。
在框架的制作过程中,我们为了满足一些常见需求,对一些部分进行了处理,形成了一些潜在的约定。尽管这些约定并非强制性的,但它们有助于提高开发效率和代码质量
后端
1. 逻辑删除
在创建数据表时,我们推荐使用以下命名和物理类型来实现逻辑删除:
`del_flag` enum('T','F') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'F' COMMENT '删除与否',
`delete_id` int DEFAULT NULL COMMENT '(逻辑)删除人',
`delete_time` datetime DEFAULT NULL COMMENT '(逻辑)删除时间',
这样的设计可以清晰地表示记录的删除状态,并且在数据库层面提供了直观的标识。
除了数据库,我们也需要对PO类进行处理:
@Table("sys_user")
@Schema(description = "系统用户表")
public class SysUser implements Serializable {
...
@Column(isLogicDelete = true) // 通过@Column来指定逻辑删除属性
private String delFlag;
...
}
我们的Mybatis-Flex也指定了逻辑删除标识为T、F字符。
mybatis-flex:
configuration:
map-underscore-to-camel-case: true
jdbc-type-for-null: null
auto-mapping-behavior: full
auto-mapping-unknown-column-behavior: none
cache-enabled: false
global-config:
deleted-value-of-logic-delete: "T" # 逻辑删除字段的已删除值
normal-value-of-logic-delete: "F" # 逻辑删除字段的未删除值
同时,我们也提供了逻辑删除数据填充的支持:
public class EntityLogicDeleteListener extends DefaultLogicDeleteProcessor {
private static final String FIELD_DELETE_TIME = "delete_time";
private static final String FIELD_DELETE_ID = "delete_id";
@Override
public String buildLogicDeletedSet(String logicColumn, TableInfo tableInfo, IDialect iDialect) {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append(iDialect.wrap(logicColumn))
.append(EQUALS)
.append(prepareValue(getLogicDeletedValue()));
List<String> columns = Arrays.asList(tableInfo.getAllColumns());
if (columns.contains(FIELD_DELETE_TIME)) {
sqlBuilder.append(", ")
.append(iDialect.wrap(FIELD_DELETE_TIME))
.append(EQUALS)
.append("now()");
}
boolean isLogin = StpUtil.isLogin();
if (isLogin && columns.contains(FIELD_DELETE_ID)) {
sqlBuilder.append(", ")
.append(iDialect.wrap(FIELD_DELETE_ID))
.append(EQUALS)
.append(LoginUtils.getLoginUser().getUserInfo().getId());
}
return sqlBuilder.toString();
}
private static Object prepareValue(Object value) {
return (!(value instanceof Number) && !(value instanceof Boolean)) ? "'" + value + "'" : value;
}
}
2. 数据填充
当我们操作某些表时,有时候我们有可能需要根据某些情况进行一些更新操作。 例如 Insert操作需要更新create_id 和 create_time、Update操作要更新update_id和 update_time。
针对于常见的这四个属性,框架提供了默认的数据填充支持。
在创建数据表时,我们推荐使用以下命名和物理类型来实现数据填充:
以下四个属性根据业务情况自行组合,系统会自动根据命名匹配并填充。
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
`create_id` bigint DEFAULT NULL COMMENT '创建人',
`update_id` bigint DEFAULT NULL COMMENT '更新人',
针对sys_client表的数据填充需求,我们建议按照以下示例对PO类进行命名和类型处理
注意!!
必须遵照以下类型,否则动态填充会异常!!!
@Table(value = "sys_client", onInsert = EntityChangeListener.class, onUpdate = EntityChangeListener.class)
public class SysClient implements Serializable {
...
@Schema(description ="创建者")
private Long createId;
@Schema(description ="创建时间")
private LocalDateTime createTime;
@Schema(description ="更新者")
private Long updateId;
@Schema(description ="更新时间")
private LocalDateTime updateTime;
...
}
这样的处理方式可以清晰地描述这些属性的作用,并为数据填充提供了明确的指导。
那么我们系统中是如何实现“动态填充”的呢?
public class EntityChangeListener implements InsertListener, UpdateListener, SetListener {
@Override
public void onInsert(Object o) {
setPropertyIfPresent(o, "createId", StpUtil.getLoginIdAsLong());
setPropertyIfPresent(o, "createTime", LocalDateTime.now());
}
@Override
public void onUpdate(Object o) {
setPropertyIfPresent(o, "updateId", StpUtil.getLoginIdAsLong());
setPropertyIfPresent(o, "updateTime", LocalDateTime.now());
}
@Override
public Object onSet(Object entity, String property, Object value) {
return value;
}
private void setPropertyIfPresent(Object object, String propertyName, Object propertyValue) {
try {
// 获取对象的 Class 对象
Class<?> clazz = object.getClass();
// 获取对象的字段
Field field = clazz.getDeclaredField(propertyName);
// 设置字段为可访问,以便访问私有字段
field.setAccessible(true);
// 设置字段的值
field.set(object, propertyValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
// 如果字段不存在,则忽略异常
log.warn(" Fill EntityChangeField failed; Property {} not found. ", propertyName);
}
}
}
在我们的系统中,我们通过一个名为 EntityChangeListener
的类来实现动态填充的功能。这个类实现了 InsertListener
、UpdateListener
和 SetListener
接口,分别用于在插入、更新和设置操作时进行监听和处理。
在 onInsert
方法中,我们会动态设置对象的 createId
和 createTime
属性,以记录创建者和创建时间。而在 onUpdate
方法中,我们会动态设置对象的 updateId
和 updateTime
属性,以记录最后更新者和更新时间。
为了实现动态设置属性的功能,我们使用了反射机制来访问对象的属性并设置属性的值。具体来说,我们通过获取对象的 Class 对象和字段对象,并设置字段为可访问状态,然后利用反射机制设置字段的值。
需要注意的是,我们在 setPropertyIfPresent
方法中对属性的设置进行了容错处理,如果属性不存在,则会忽略异常并记录警告日志,以确保程序的稳定性和可靠性。
通过这种方式,我们实现了动态填充的功能,为我们的系统提供了便利和灵活性。
3. SQL的编写
我们的框架集成了Mybatis-Flex,这不仅支持传统的原生XML编写SQL,还引入了更为高效灵活的查询构建方式。
您可以通过以下两种方式来构建SQL查询:
- Mybatis-Flex:一种更现代、易于理解和维护的方法,适用于绝大多数查询场景。
- 原生XML:在特定情况下,如果需要,您也可以使用传统的XML方式。
虽然我们在这里不展开介绍Mybatis-Flex的具体用法,但强烈推荐您优先采用Mybatis-Flex。它能够满足绝大部分的查询需求,并且通常更加简洁和直观。
如需深入了解Mybatis-Flex的使用方法,您可以访问官方网站获取详细信息。
在遇到特殊情况,原生XML方式可能是必要的选择。但请记住,大多数时候,Mybatis-Flex将是您的首选。
4. 数据权限
详见:数据权限