数据库规范
数据库规范在阿里规约的基础上,进一步结合标品实际情况,加以说明。
数据库规范。持续补充...
公共字段实现说明
目前标品存在两种情况:
基于基础类
BaseEntity
实现的数据实体基于这个类的实现类,是早期的代码实现类。他的公共字段包含如下:
名称 简介 id 主键 status 删除状态 createUser 创建人 gmtCreated 创建时间 gmtModified 修改时间 modifyUser 修改人 基于基础类
CreationAuditedAggregateRoot
实现的数据实体基于这个类的实现类,是目前最新的代码实现类。他的公共字段包含如下:
名称 简介 id 主键 status 状态 createUser 创建人 gmtCreated 创建时间 gmtModified 修改时间 modifyUser 修改人 deleted 是否删除 多了个 deleted
字段。
为什么要这么做呢?这是因为如果只使用BaseEntity
基础类,我们在日常编写业务时,需要时刻记住为每个查询都增加status=1
的过滤操作,要是少加了,可能就是一个BUG
- 为什么这个数据删除了还能显示?
- 为什么这个数据删除了,还不能怎么样?
- 等等类似问题
那直接使用CreationAuditedAggregateRoot
基础类不用加吗?是的,不需要加。我只要加个@LoginDelete
注解便可以实现。好处就是天然屏蔽上面所述的问题。
status 和 deleted 字段共存的问题说明
查阅标品代码里能发现多个模块里存在status
和deleted
共存的代码。一部分原因上面解释了,另外一个原因是status
是基础jar
包里提供的,这个不能直接去,因为为了兼容处理而做了保留。
- 当项目使用
CreationAuditedAggregateRoot
基础类实体时,deleted
字段表示删除状态。自动由@LoginDelete
注解实现,而status
字段无意义。 - 当项目使用
BaseEntity
基础类实体时,无deleted
字段,status
字段还是原来的含义,表示删除状态。
后续已有的模块都应该要调整成一种实现。
如何赋值
id 策略由具体业务来指定。标品不强制指定。默认自增。而其他几个公共字段,依赖的是基于mybatis
的拦截器实现的自动赋值。代码如下:
/**
* MybatisInterceptor 自动处理数据库 entity 中的创建人、创建时间、修改人、修改时间
*
* @author mjyang
*/
@Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
Object.class}))
public class MybatisInterceptor implements Interceptor {
...
}
在标品项目里可以找到MybatisInterceptor
该拦截器的完整实现。
最佳实现建议
结合上面的注解和拦截器,我们可以发现,对于公共字段如何操作,以及删除后的数据查询自动过滤,我们都不用操心,也不用担心出错。这样可以使我们的关注点更加集中在业务的分析上。除此之外,我们有时还会有类似多租户的需求,对于此,我们通过分析可以知道,所有表或是部分表需要在增加一个公共字段的话,一个可扩展统一的实体基础类模型是有多么重要,我可以轻松的在这个基础类上再做一层包装,轻松的就能增加一个公共类字段出来,那剩下的是不是就是查询需要手动在去增加这个字段。想想看,这个地方是不是和手动加status=1
操作一样呢?没错的,可以利用拦截器统一来实现,接下来将给出一个模板拦截器:
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2022 abel533@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.pagehelper;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* QueryInterceptor 规范
*
* 详细说明见文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md
*
* @author liuzh/abel533/isea533
* @version 1.0.0
*/
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class QueryInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if(args.length == 4){
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
//TODO 自己要进行的各种处理
//注:下面的方法可以根据自己的逻辑调用多次,在分页插件中,count 和 page 各调用了一次
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
这个是PageHelper
官网一个最佳推荐,我们只要按照这个模块来定制就可以了。
以下是一个基于多语言需求实现的拦截器,核心代码如下:
package com.wmeimob.mall.service.configuration.mybatis.language.interceptor;
import com.github.pagehelper.util.ExecutorUtil;
import com.github.pagehelper.util.MetaObjectUtil;
import com.wmeimob.mall.service.configuration.mybatis.language.LanguageLineHandler;
import com.wmeimob.mall.service.configuration.mybatis.language.util.LanguageUtil;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.builder.annotation.ProviderSqlSource;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* 多语言拦截器
*
* @author mjyang
* @date 2022/4/1 11:02
*/
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class,
Object.class, RowBounds.class,
ResultHandler.class
}),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class,
Object.class,
RowBounds.class,
ResultHandler.class,
CacheKey.class,
BoundSql.class
}),
}
)
@RequiredArgsConstructor
public class LanguageInterceptor implements Interceptor {
private final LanguageLineHandler languageLineHandler;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
if (LanguageUtil.ignore()) {
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
String originalSql = boundSql.getSql();
Statement statement = CCJSqlParserUtil.parse(originalSql);
PlainSelect plainSelect = (PlainSelect) ((Select) statement).getSelectBody();
String tableName = plainSelect.getFromItem().toString();
if (languageLineHandler.ignoreTable(tableName)) {
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
//处理参数对象
parameter = processParameterObject(ms, parameter, boundSql, cacheKey);
//处理sql
String newSql = getSql(originalSql, plainSelect);
BoundSql languageBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = ExecutorUtil.getAdditionalParameter(boundSql);
//设置动态参数
for (String key : additionalParameters.keySet()) {
languageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, languageBoundSql);
}
private String getSql(String originalSql, PlainSelect plainSelect) {
String newSql;
if (plainSelect.getWhere() == null) {
newSql = originalSql + String.format(" WHERE %s = ?", languageLineHandler.getLanguageColumn());
} else {
newSql = originalSql + String.format(" AND %s = ?", languageLineHandler.getLanguageColumn());
}
return newSql;
}
private Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey cacheKey) {
Map<String, Object> paramMap;
if (parameterObject == null) {
paramMap = new HashMap<>();
} else if (parameterObject instanceof Map) {
//解决不可变Map的情况
paramMap = new HashMap<>();
paramMap.putAll((Map) parameterObject);
} else {
paramMap = new HashMap<>();
// sqlSource为ProviderSqlSource时,处理只有1个参数的情况
if (ms.getSqlSource() instanceof ProviderSqlSource) {
String[] providerMethodArgumentNames = ExecutorUtil.getProviderMethodArgumentNames((ProviderSqlSource) ms.getSqlSource());
if (providerMethodArgumentNames != null && providerMethodArgumentNames.length == 1) {
paramMap.put(providerMethodArgumentNames[0], parameterObject);
paramMap.put("param1", parameterObject);
}
}
//动态sql时的判断条件不会出现在ParameterMapping中,但是必须有,所以这里需要收集所有的getter属性
//TypeHandlerRegistry可以直接处理的会作为一个直接使用的对象进行处理
boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
//需要针对注解形式的MyProviderSqlSource保存原值
if (!hasTypeHandler) {
for (String name : metaObject.getGetterNames()) {
paramMap.put(name, metaObject.getValue(name));
}
}
}
return processLanguageParameter(ms, paramMap, boundSql, cacheKey);
}
private Object processLanguageParameter(MappedStatement ms, Map<String, Object> paramMap, BoundSql boundSql, CacheKey cacheKey) {
paramMap.put(languageLineHandler.getLanguageColumn(), languageLineHandler.getValue());
//处理 cacheKey
cacheKey.update(languageLineHandler.getValue());
//处理参数
if (boundSql.getParameterMappings() != null) {
List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), languageLineHandler.getLanguageColumn(), String.class).build());
MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
metaObject.setValue("parameterMappings", newParameterMappings);
}
return paramMap;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
可以看到,基于模板拦截器,我们要实现一个自定义通用的拦截器就变得简单多了。基于此多语言拦截器实现了自动为给定表增加language
查询字段,同时实现了参数化查询,避免sql
注入。