背景介绍
公司最近做分库分表业务,接入了 Sharding JDBC,接入完成后,回归测试时发现好几个 SQL 执行报错,关键这几个表都还不是分片表。
报错如下:
这下糟了嘛。熟悉 Sharding JDBC 的同学应该知道,有很多 SQL 它是不支持的。官方截图如下:
如果要去修改这些复杂 SQL 的话,可能要花费很多时间。那怎么办呢?只能从 Sharding JDBC 这里找突破口了,两天的研究,出来了下面这个只需要加一个注解轻松解决 Sharding Jdbc 不支持复杂 SQL 的方案。
问题复现
我本地写了一个复杂 SQL 进行测试:
public List
public interface BOrderRepository extends JpaRepository{ @Query(value = "SELECT * FROM (SELECT id,CASE WHEN company_id =1 THEN '小' WHEN company_id=4 THEN '中' ELSE '大' END AS com,user_id as userId FROM b_order0) t WHERE t.com ='中'",nativeQuery =true) List > findOrders(); }
写了个测试 controller 来调用,调用后果然报错了。
解决思路
因为查询的复杂 SQL 的表不是分片表,那能不能指定这几个复杂查询的时候不用 Sharding JDBC 的数据源呢?
- 在注入 Sharding JDBC 数据源的地方做处理,注入一个我们自定义的数据源
- 这样我们获取连接的时候就能返回原生数据源了
- 另外我们声明一个注解,对标识了注解的就返回原生数据源,否则还是返回 Sharding 数据源
具体实现
编写autoConfig 类
- 编写一个 autoConfig 类,来替换 ShardingSphereAutoConfiguration 类
/** * 动态数据源核心自动配置类 * * */ @Configuration @ComponentScan("org.apache.shardingsphere.spring.boot.converter") @EnableConfigurationProperties(SpringBootPropertiesConfiguration.class) @ConditionalOnProperty(prefix = "spring.shardingsphere", name = "enabled", havingValue = "true", matchIfMissing = true) @AutoConfigureBefore(DataSourceAutoConfiguration.class) public class DynamicDataSourceAutoConfiguration implements EnvironmentAware { private String databaseName; private final SpringBootPropertiesConfiguration props; private final MapdataSourceMap = new LinkedHashMap<>(); public DynamicDataSourceAutoConfiguration(SpringBootPropertiesConfiguration props) { this.props = props; } /** * Get mode configuration. * * @return mode configuration */ @Bean public ModeConfiguration modeConfiguration() { return null == props.getMode() ? null : new ModeConfigurationYamlSwapper().swapToObject(props.getMode()); } /** * Get ShardingSphere data source bean. * * @param rules rules configuration * @param modeConfig mode configuration * @return data source bean * @throws SQLException SQL exception */ @Bean @Conditional(LocalRulesCondition.class) @Autowired(required = false) public DataSource shardingSphereDataSource(final ObjectProvider > rules, final ObjectProvider
modeConfig) throws SQLException { Collection ruleConfigs = Optional.ofNullable(rules.getIfAvailable()).orElseGet(Collections::emptyList); DataSource dataSource = ShardingSphereDataSourceFactory.createDataSource(databaseName, modeConfig.getIfAvailable(), dataSourceMap, ruleConfigs, props.getProps()); return new WrapShardingDataSource((ShardingSphereDataSource) dataSource,dataSourceMap); } /** * Get data source bean from registry center. * * @param modeConfig mode configuration * @return data source bean * @throws SQLException SQL exception */ @Bean @ConditionalOnMissingBean(DataSource.class) public DataSource dataSource(final ModeConfiguration modeConfig) throws SQLException { DataSource dataSource = !dataSourceMap.isEmpty() ? ShardingSphereDataSourceFactory.createDataSource(databaseName, modeConfig, dataSourceMap, Collections.emptyList(), props.getProps()) : ShardingSphereDataSourceFactory.createDataSource(databaseName, modeConfig); return new WrapShardingDataSource((ShardingSphereDataSource) dataSource,dataSourceMap); } /** * Create transaction type scanner. * * @return transaction type scanner */ @Bean public TransactionTypeScanner transactionTypeScanner() { return new TransactionTypeScanner(); } @Override public final void setEnvironment(final Environment environment) { dataSourceMap.putAll(DataSourceMapSetter.getDataSourceMap(environment)); databaseName = DatabaseNameSetter.getDatabaseName(environment); } @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @Bean @ConditionalOnProperty(prefix = "spring.datasource.dynamic.aop", name = "enabled", havingValue = "true", matchIfMissing = true) public Advisor dynamicDatasourceAnnotationAdvisor() { DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(true); DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class); return advisor; } }
自定义数据源
public class WrapShardingDataSource extends AbstractDataSourceAdapter implements AutoCloseable{ private ShardingSphereDataSource dataSource; private Map<String, DataSource> dataSourceMap; public WrapShardingDataSource(ShardingSphereDataSource dataSource, Map<String, DataSource> dataSourceMap) { this.dataSource = dataSource; this.dataSourceMap = dataSourceMap; } public DataSource getTargetDataSource(){ String peek = DynamicDataSourceContextHolder.peek(); if(StringUtils.isEmpty(peek)){ return dataSource; } return dataSourceMap.get(peek); } @Override public Connection getConnection() throws SQLException { return getTargetDataSource().getConnection(); } @Override public Connection getConnection(final String username, final String password) throws SQLException { return getConnection(); } @Override public void close() throws Exception { DataSource targetDataSource = getTargetDataSource(); if (targetDataSource instanceof AutoCloseable) { ((AutoCloseable) targetDataSource).close(); } } @Override public int getLoginTimeout() throws SQLException { DataSource targetDataSource = getTargetDataSource(); return targetDataSource ==null ? 0 : targetDataSource.getLoginTimeout(); } @Override public void setLoginTimeout(final int seconds) throws SQLException { DataSource targetDataSource = getTargetDataSource(); targetDataSource.setLoginTimeout(seconds); } }
- 声明指定数据源注解
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DS { /** * 数据源名 */ String value(); }
- 另外使用 AOP 的方式拦截使用了注解的类或方法,并且要将这些用了注解的方法存起来,在获取数据源连接的时候取出来进行判断。这就还要用到 ThreadLocal。
aop 拦截器:
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { private final DataSourceClassResolver dataSourceClassResolver; public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly) { dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly); } @Override public Object invoke(MethodInvocation invocation) throws Throwable { String dsKey = determineDatasourceKey(invocation); DynamicDataSourceContextHolder.push(dsKey); try { return invocation.proceed(); } finally { DynamicDataSourceContextHolder.poll(); } } private String determineDatasourceKey(MethodInvocation invocation) { String key = dataSourceClassResolver.findKey(invocation.getMethod(), invocation.getThis()); return key; } }
aop 切面定义
/** * aop Advisor */ public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { private final Advice advice; private final Pointcut pointcut; private final Class extends Annotation> annotation; public DynamicDataSourceAnnotationAdvisor(MethodInterceptor advice, Class extends Annotation> annotation) { this.advice = advice; this.annotation = annotation; this.pointcut = buildPointcut(); } @Override public Pointcut getPointcut() { return this.pointcut; } @Override public Advice getAdvice() { return this.advice; } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (this.advice instanceof BeanFactoryAware) { ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); } } private Pointcut buildPointcut() { Pointcut cpc = new AnnotationMatchingPointcut(annotation, true); Pointcut mpc = new AnnotationMethodPoint(annotation); return new ComposablePointcut(cpc).union(mpc); } /** * In order to be compatible with the spring lower than 5.0 */ private static class AnnotationMethodPoint implements Pointcut { private final Class extends Annotation> annotationType; public AnnotationMethodPoint(Class extends Annotation> annotationType) { Assert.notNull(annotationType, "Annotation type must not be null"); this.annotationType = annotationType; } @Override public ClassFilter getClassFilter() { return ClassFilter.TRUE; } @Override public MethodMatcher getMethodMatcher() { return new AnnotationMethodMatcher(annotationType); } private static class AnnotationMethodMatcher extends StaticMethodMatcher { private final Class extends Annotation> annotationType; public AnnotationMethodMatcher(Class extends Annotation> annotationType) { this.annotationType = annotationType; } @Override public boolean matches(Method method, Class> targetClass) { if (matchesMethod(method)) { return true; } // Proxy classes never have annotations on their redeclared methods. if (Proxy.isProxyClass(targetClass)) { return false; } // The method may be on an interface, so let's check on the target class as well. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); return (specificMethod != method && matchesMethod(specificMethod)); } private boolean matchesMethod(Method method) { return AnnotatedElementUtils.hasAnnotation(method, this.annotationType); } } } }
/** * 数据源解析器 * */ public class DataSourceClassResolver { private static boolean mpEnabled = false; private static Field mapperInterfaceField; static { Class> proxyClass = null; try { proxyClass = Class.forName("com.baomidou.mybatisplus.core.override.MybatisMapperProxy"); } catch (ClassNotFoundException e1) { try { proxyClass = Class.forName("com.baomidou.mybatisplus.core.override.PageMapperProxy"); } catch (ClassNotFoundException e2) { try { proxyClass = Class.forName("org.apache.ibatis.binding.MapperProxy"); } catch (ClassNotFoundException ignored) { } } } if (proxyClass != null) { try { mapperInterfaceField = proxyClass.getDeclaredField("mapperInterface"); mapperInterfaceField.setAccessible(true); mpEnabled = true; } catch (NoSuchFieldException e) { e.printStackTrace(); } } } /** * 缓存方法对应的数据源 */ private final Map
ThreadLocal
public final class DynamicDataSourceContextHolder { /** * 为什么要用链表存储(准确的是栈) ** 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。 **/ private static final ThreadLocal> LOOKUP_KEY_HOLDER = new NamedThreadLocal >("dynamic-datasource") { @Override protected Deque initialValue() { return new ArrayDeque<>(); } }; private DynamicDataSourceContextHolder() { } /** * 获得当前线程数据源 * * @return 数据源名称 */ public static String peek() { return LOOKUP_KEY_HOLDER.get().peek(); } /** * 设置当前线程数据源 * * 如非必要不要手动调用,调用后确保最终清除 *
* * @param ds 数据源名称 */ public static String push(String ds) { String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds; LOOKUP_KEY_HOLDER.get().push(dataSourceStr); return dataSourceStr; } /** * 清空当前线程数据源 ** 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 *
*/ public static void poll() { Dequedeque = LOOKUP_KEY_HOLDER.get(); deque.poll(); if (deque.isEmpty()) { LOOKUP_KEY_HOLDER.remove(); } } /** * 强制清空本地线程 * * 防止内存泄漏,如手动调用了push可调用此方法确保清除 *
*/ public static void clear() { LOOKUP_KEY_HOLDER.remove(); } }
启动类配置
引入我们写的自动配置类,排除 ShardingJdbc 的自动配置类。
@SpringBootApplication(exclude = ShardingSphereAutoConfiguration.class) @Import({DynamicDataSourceAutoConfiguration.class}) public class ShardingRunApplication { public static void main(String[] args) { SpringApplication.run(ShardingRunApplication.class); } }
最后,我们给之前写的 Repository 加上注解:
public interface BOrderRepository extends JpaRepository{ @DS("slave0") @Query(value = "SELECT * FROM (SELECT id,CASE WHEN company_id =1 THEN '小' WHEN company_id=4 THEN '中' ELSE '大' END AS com,user_id as userId FROM b_order0) t WHERE t.com ='中'",nativeQuery =true) List > findOrders(); }
再次调用,查询成功!!!
以上就是使用注解解决ShardingJdbc不支持复杂SQL方法的详细内容,更多关于ShardingJdbc不支持复杂SQL的资料请关注脚本之家其它相关文章!