Skip to content

数据库规范

数据库规范在阿里规约的基础上,进一步结合标品实际情况,加以说明。

数据库规范。持续补充...

公共字段实现说明

目前标品存在两种情况:

  • 基于基础类BaseEntity实现的数据实体

    基于这个类的实现类,是早期的代码实现类。他的公共字段包含如下:

    名称简介
    id主键
    status删除状态
    createUser创建人
    gmtCreated创建时间
    gmtModified修改时间
    modifyUser修改人
  • 基于基础类CreationAuditedAggregateRoot实现的数据实体

    基于这个类的实现类,是目前最新的代码实现类。他的公共字段包含如下:

    名称简介
    id主键
    status状态
    createUser创建人
    gmtCreated创建时间
    gmtModified修改时间
    modifyUser修改人
    deleted是否删除
    多了个deleted字段。

为什么要这么做呢?这是因为如果只使用BaseEntity基础类,我们在日常编写业务时,需要时刻记住为每个查询都增加status=1的过滤操作,要是少加了,可能就是一个BUG

  • 为什么这个数据删除了还能显示?
  • 为什么这个数据删除了,还不能怎么样?
  • 等等类似问题

那直接使用CreationAuditedAggregateRoot基础类不用加吗?是的,不需要加。我只要加个@LoginDelete注解便可以实现。好处就是天然屏蔽上面所述的问题。

status 和 deleted 字段共存的问题说明

查阅标品代码里能发现多个模块里存在statusdeleted共存的代码。一部分原因上面解释了,另外一个原因是status是基础jar包里提供的,这个不能直接去,因为为了兼容处理而做了保留。

  • 当项目使用CreationAuditedAggregateRoot基础类实体时,deleted字段表示删除状态。自动由@LoginDelete注解实现,而status字段无意义。
  • 当项目使用BaseEntity基础类实体时,无deleted字段,status字段还是原来的含义,表示删除状态。

后续已有的模块都应该要调整成一种实现。

如何赋值

id 策略由具体业务来指定。标品不强制指定。默认自增。而其他几个公共字段,依赖的是基于mybatis的拦截器实现的自动赋值。代码如下:

java
/**
 * MybatisInterceptor 自动处理数据库 entity 中的创建人、创建时间、修改人、修改时间
 *
 * @author mjyang
 */
@Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
        Object.class}))
public class MybatisInterceptor implements Interceptor {
    ...
}

在标品项目里可以找到MybatisInterceptor该拦截器的完整实现。

最佳实现建议

结合上面的注解和拦截器,我们可以发现,对于公共字段如何操作,以及删除后的数据查询自动过滤,我们都不用操心,也不用担心出错。这样可以使我们的关注点更加集中在业务的分析上。除此之外,我们有时还会有类似多租户的需求,对于此,我们通过分析可以知道,所有表或是部分表需要在增加一个公共字段的话,一个可扩展统一的实体基础类模型是有多么重要,我可以轻松的在这个基础类上再做一层包装,轻松的就能增加一个公共类字段出来,那剩下的是不是就是查询需要手动在去增加这个字段。想想看,这个地方是不是和手动加status=1操作一样呢?没错的,可以利用拦截器统一来实现,接下来将给出一个模板拦截器:

java
/*
 * 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官网一个最佳推荐,我们只要按照这个模块来定制就可以了。

以下是一个基于多语言需求实现的拦截器,核心代码如下:

java
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注入。