多租户就是用额外的一个字段代表当前表中的数据的归属。在sql curd时根据上下文的用户(租户) 自动拼接此条件
动态就是有些表是公用的,没有多租户的概念。那么操作此表的时候需要排除,亦或者多个字段,每个字段的值都不一样等
前期工作
1.首先定义一个多租户字段的枚举,为提取数据库多租户字段的字段做准备
IEnums参考通用枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Getter public enum TenantField implements IEnums<String> { MID("商户id", "mid", () -> CurrentTenant.getCurrentTenant().getMid()), ; private final String[] dbFieldNames; private final String doc; private final Supplier<Long> getTenantValue;
TenantField(String doc, String underlineField, Supplier<Long> getTenantValue) { this.doc = doc; this.getTenantValue = getTenantValue; String underlineLower = underlineField.toLowerCase(Locale.ENGLISH); String underlineUpper = underlineField.toUpperCase(Locale.ENGLISH); String camel = StringUtils.underlineToCamel(underlineField); String camelLower = camel.toLowerCase(Locale.ENGLISH); String camelUpper = camel.toUpperCase(Locale.ENGLISH); this.dbFieldNames = new String[]{underlineLower, underlineUpper, camel, camelLower, camelUpper}; }
@Override public String[] getIdentities() { return dbFieldNames; }
@Override public String getDoc() { return doc; } }
|
定义好字段,以及获取字段值的方式之后接下来该读取数据库有此字段的表
为接下来动态拼接sql做准备
读取数据库的多租户信息
这个是MYSQL的获取表字段的方式哦,其他类型的数据库请参考其文档
LogUtils.lazyJson 可参考优雅打印日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Autowired DataSource dataSource;
private final Map<String, List<String>> tableName$tenantField_map = new CaseInsensitiveKeyMap<>();
@Override public void afterPropertiesSet() throws Exception { Set<String> tenantColumnNameSet = Arrays.stream(TenantField.values()) .map(TenantField::getDbFieldNames) .flatMap(Arrays::stream) .collect(Collectors.toSet()); log.info("tenant init all supports column names:\n{}", LogUtils.lazyJson(tenantColumnNameSet));
try (Connection connection = dataSource.getConnection()) { String catalog = connection.getCatalog(); DatabaseMetaData metaData = connection.getMetaData(); ResultSet tables = metaData.getTables(catalog, null, null, new String[]{"TABLE"});
while (tables.next()) { String table_name = tables.getString("TABLE_NAME"); ResultSet columns = metaData.getColumns(catalog, null, table_name, null); while (columns.next()) { String column_name = columns.getString("COLUMN_NAME"); if (tenantColumnNameSet.contains(column_name)) { tableName$tenantField_map.computeIfAbsent(table_name, k -> Lists.newArrayList()) .add(column_name); } } } } log.info("tenant init table name and tenant column name :\n{}", LogUtils.lazyJson(tableName$tenantField_map)); }
|
提取表对应的租户字段之后就可以做动态注入的操作操作了。
如果表结构变更,只有重启系统才会生效哦
拦截增删改查sql并动态注入条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
| @Component public class CustomTenantSqlParser extends TenantSqlParser {
@Override public void processInsert(net.sf.jsqlparser.statement.insert.Insert insert) { ItemsList itemsList = insert.getItemsList(); if (itemsList == null) { return; }
List<String> tenantFieldList = ObjectUtils.defaultIfNull(tableName$tenantField_map.get(insert.getTable().getName()), Collections.emptyList());
Set<String> existsColumnsSet = insert.getColumns().stream() .map(Column::getColumnName) .collect(Collectors.toSet());
tenantFieldList = tenantFieldList.stream() .filter(t -> !existsColumnsSet.contains(t)) .collect(Collectors.toList());
if (CollectionUtils.isEmpty(tenantFieldList) || !CurrentTenant.isValidTenant()) { return; }
List<Column> newColumnList = tenantFieldList.stream() .map(Column::new) .collect(Collectors.toList()); insert.getColumns().addAll(newColumnList);
List<Expression> valueExpressionList = tenantFieldList.stream() .map(field -> getTenantValueExpression(IEnums.mustGetEnum(TenantField.class, field))) .collect(Collectors.toList());
if (itemsList instanceof MultiExpressionList) { ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().addAll(valueExpressionList)); } else { ((ExpressionList) insert.getItemsList()).getExpressions().addAll(valueExpressionList); } }
@Override public void processUpdate(net.sf.jsqlparser.statement.update.Update update) { Expression expression = getExpression(update.getTable(), update.getWhere()); if (expression != null) { update.setWhere(expression); } }
@Override public void processDelete(net.sf.jsqlparser.statement.delete.Delete delete) { Expression expression = getExpression(delete.getTable(), delete.getWhere()); if (expression != null) { delete.setWhere(expression); } }
private Expression getExpression(Table table, Expression where) { List<String> tenantFieldList = tableName$tenantField_map.get(table.getName()); return builderExpression(where, table, tenantFieldList); }
@Override protected void processPlainSelect(PlainSelect plainSelect, boolean addColumn) { FromItem fromItem = plainSelect.getFromItem(); if (fromItem instanceof Table) { Table fromTable = (Table) fromItem; List<String> tenantFieldList = tableName$tenantField_map.get(fromTable.getName()); plainSelect.setWhere(builderExpression(plainSelect.getWhere(), fromTable, tenantFieldList)); if (addColumn) { tenantFieldList.forEach(field -> plainSelect.getSelectItems().add(new SelectExpressionItem(new Column(field)))); } } else { processFromItem(fromItem); } List<Join> joins = plainSelect.getJoins(); if (joins != null && joins.size() > 0) { joins.forEach(j -> { processJoin(j); processFromItem(j.getRightItem()); }); } }
@Override protected void processJoin(Join join) { if (join.getRightItem() instanceof Table) { Table rightItem = (Table) join.getRightItem(); List<String> tenantFieldList = tableName$tenantField_map.get(rightItem.getName()); join.setOnExpression(builderExpression(join.getOnExpression(), rightItem, tenantFieldList)); } }
private Expression builderExpression(Expression currentExpression, Table table, List<String> tenantFieldList) { if (CollectionUtils.isEmpty(tenantFieldList) || !CurrentTenant.isValidTenant()) { return currentExpression; }
if (currentExpression instanceof BinaryExpression) { BinaryExpression binaryExpression = (BinaryExpression) currentExpression; doExpression(binaryExpression.getLeftExpression()); doExpression(binaryExpression.getRightExpression()); } else if (currentExpression instanceof InExpression) { InExpression inExp = (InExpression) currentExpression; ItemsList rightItems = inExp.getRightItemsList(); if (rightItems instanceof SubSelect) { processSelectBody(((SubSelect) rightItems).getSelectBody()); } ItemsList leftItems = inExp.getLeftItemsList(); if (leftItems instanceof SubSelect) { processSelectBody(((SubSelect) leftItems).getSelectBody()); } }
Expression expression = currentExpression; for (String tenantField : tenantFieldList) { Expression tenantValueExpression = getTenantValueExpression(IEnums.mustGetEnum(TenantField.class, tenantField));
Expression appendExpression = this.processTableAlias4CustomizedTenantIdExpression(tenantValueExpression, table, tenantField); if (expression instanceof OrExpression) { expression = new AndExpression(appendExpression, new Parenthesis(expression)); } else if (expression != null) { expression = new AndExpression(appendExpression, expression); } else { expression = appendExpression; }
} return expression; }
private Expression processTableAlias4CustomizedTenantIdExpression(Expression expression, Table table, String tenantField) { if (expression instanceof ValueListExpression) { InExpression inExpression = new InExpression(); inExpression.setLeftExpression(this.getAliasColumn(table, tenantField)); inExpression.setRightItemsList(((ValueListExpression) expression).getExpressionList()); return inExpression; } else { EqualsTo equalsTo = new EqualsTo(); equalsTo.setLeftExpression(this.getAliasColumn(table, tenantField)); equalsTo.setRightExpression(expression); return equalsTo; } }
private Column getAliasColumn(Table table, String tenantField) { StringBuilder column = new StringBuilder(); if (table.getAlias() != null) { column.append(table.getAlias().getName()).append(StringPool.DOT); } column.append(tenantField); return new Column(column.toString()); }
private static Expression getTenantValueExpression(TenantField tenantField) { Long tenantId = tenantField.getGetTenantValue().get(); return new LongValue(tenantId); }
}
|
实现增删改方法的拦截之后再把此类注入到spring容器当中即可
注入并生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Configuration public class MyBatisPlusConfig {
@Autowired CustomTenantSqlParser customTenantSqlParser;
@Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); paginationInterceptor.setSqlParserList(Collections.singletonList(customTenantSqlParser)); return paginationInterceptor; }
}
|
总结
至此动态多租户的插件到此结束,总结分为这几个步骤
1.定义好多租户的字段以及值获取的方式(enum TenantField)
2.根据定义好的字段读取数据库有此字段的表的信息,定义一个java缓存(tableName$tenantField_map)
3.实现多租户sql拦截的方法包括:增(自动insert)删改查(自动拼接sql条件)
4.把此插件注入到spring容器中即可
实现的效果为所有的sql 会自动拼接对应的条件。但是具体字段名称和字段值的提供都由使用者自定义实现(enum TenantField)
这样我们针对saas系统的业务完全可以当做非saas系统来开发,提升开发效率