【问题标题】:Spring JdbcTemplate - how to prepend every query for achieving multitenancy?Spring JdbcTemplate - 如何预先为实现多租户的每个查询?
【发布时间】:2019-03-03 15:04:06
【问题描述】:

设置

我有一个使用 Spring 4.3、JdbcTemplate、Hibernate 5 和 MySQL 8 的应用程序。我已经在 hibernate 每个模式中实现了多租户,我使用 hibernate 多租户机制切换模式 - MultiTenantConnectionProvider 并基本上在那里做:

connection.createStatement().execute("USE " + databaseNamePrefix + tenantIdentifier); 

这行得通。

现在我的应用程序的报告部分使用JdbcTemplate 来查询数据库。 现在我想在 JdbcTemplate 执行的每个查询之前同样发出这个USE tenantIdentifier 语句。

问题

如何在 JdbcTemplate 执行的每个查询中添加一些 SQL 或语句?

我的尝试

我查看了 JdbcTemplate,发现唯一的事情是设置 NativeJdbcExtractor。我已经尝试了下面的代码,但他甚至没有登录,他正在通过这种方法。

@Bean
@DependsOn("dataSource")
public JdbcTemplate jdbcTemplate() {
  JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource());
  jdbcTemplate.setNativeJdbcExtractor(new SimpleNativeJdbcExtractor(){
     @Override
     public Connection getNativeConnection(Connection con) throws SQLException {
        LOGGER.info("getNativeConnection");
        System.out.println("aaa");
        return super.getNativeConnection(con);
     }

     @Override
     public Connection getNativeConnectionFromStatement(Statement stmt) throws SQLException {
        System.out.println("aaa");
        LOGGER.info("getNativeConnectionFromStatement");
        return super.getNativeConnectionFromStatement(stmt);
     }

  });
  return jdbcTemplate;
}

向 Spring 添加功能请求:https://jira.spring.io/browse/SPR-17342

编辑:我查看了 Spring 5,他们删除了 JdbcExtractor 东西,所以这绝对是错误的路径。

【问题讨论】:

    标签: java spring jdbc spring-jdbc jdbctemplate


    【解决方案1】:

    已经晚了,但这是我的解决方案,也许可以帮助某人。不是很优雅,但效果很好。 我覆盖 JdbcTemplate,对于 spring 5:

    import org.springframework.dao.DataAccessException;
    import org.springframework.jdbc.core.*;
    import org.springframework.jdbc.datasource.ConnectionHolder;
    import org.springframework.jdbc.datasource.DataSourceUtils;
    import org.springframework.jdbc.support.JdbcUtils;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    import org.springframework.util.Assert;
    
    import javax.sql.DataSource;
    import java.sql.*;
    
    public class TenantJdbcTemplate extends JdbcTemplate {
    
        public TenantJdbcTemplate(DataSource dataSource) {
            super(dataSource);
        }
    
        @Override
        public <T> T execute(StatementCallback<T> action) throws DataAccessException {
            Assert.notNull(action, "Callback object must not be null");
    
            Connection con = DataSourceUtils.getConnection(obtainDataSource());
            boolean isTransactionalCon = isTransactionalConnection(con, getDataSource());
            Statement stmt = null;
            try {
                stmt = con.createStatement();
                applyStatementSettings(stmt);
                Statement stmtToUse = stmt;
                if (!isTransactionalCon) {
                    setSchema(stmtToUse);
                }
                T result = action.doInStatement(stmtToUse);
                handleWarnings(stmt);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                String sql = getSql(action);
                JdbcUtils.closeStatement(stmt);
                stmt = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw translateException("StatementCallback", sql, ex);
            }
            finally {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }
    
        @Override
        public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
                        throws DataAccessException {
    
            Assert.notNull(psc, "PreparedStatementCreator must not be null");
            Assert.notNull(action, "Callback object must not be null");
            if (logger.isDebugEnabled()) {
                String sql = getSql(psc);
                logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
            }
    
            Connection con = DataSourceUtils.getConnection(obtainDataSource());
            boolean isTransactionalCon = isTransactionalConnection(con, getDataSource());
    
            PreparedStatement ps = null;
            try {
                ps = psc.createPreparedStatement(con);
                applyStatementSettings(ps);
                if (!isTransactionalCon) {
                    try (Statement stmt = con.createStatement()) {
                        setSchema(stmt);
                    }
                }
                T result = action.doInPreparedStatement(ps);
                handleWarnings(ps);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                if (psc instanceof ParameterDisposer) {
                    ((ParameterDisposer) psc).cleanupParameters();
                }
                String sql = getSql(psc);
                psc = null;
                JdbcUtils.closeStatement(ps);
                ps = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw translateException("PreparedStatementCallback", sql, ex);
            }
            finally {
                if (psc instanceof ParameterDisposer) {
                    ((ParameterDisposer) psc).cleanupParameters();
                }
                JdbcUtils.closeStatement(ps);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }
    
        @Override
        public <T> T execute(CallableStatementCreator csc, CallableStatementCallback<T> action)
                        throws DataAccessException {
    
            Assert.notNull(csc, "CallableStatementCreator must not be null");
            Assert.notNull(action, "Callback object must not be null");
            if (logger.isDebugEnabled()) {
                String sql = getSql(csc);
                logger.debug("Calling stored procedure" + (sql != null ? " [" + sql  + "]" : ""));
            }
    
            Connection con = DataSourceUtils.getConnection(obtainDataSource());
            boolean isTransactionalCon = isTransactionalConnection(con, getDataSource());
            CallableStatement cs = null;
            try {
                cs = csc.createCallableStatement(con);
                applyStatementSettings(cs);
                if (!isTransactionalCon) {
                    try (Statement stmt = con.createStatement()) {
                        setSchema(stmt);
                    }
                }
                T result = action.doInCallableStatement(cs);
                handleWarnings(cs);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                if (csc instanceof ParameterDisposer) {
                    ((ParameterDisposer) csc).cleanupParameters();
                }
                String sql = getSql(csc);
                csc = null;
                JdbcUtils.closeStatement(cs);
                cs = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw translateException("CallableStatementCallback", sql, ex);
            }
            finally {
                if (csc instanceof ParameterDisposer) {
                    ((ParameterDisposer) csc).cleanupParameters();
                }
                JdbcUtils.closeStatement(cs);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }
    
        private static void setSchema(Statement stmt) throws SQLException {
            stmt.execute("set search_path=\"" + TenantIdHolder.getTenantId() + "\";");
        }
    
        private static String getSql(Object sqlProvider) {
            if (sqlProvider instanceof SqlProvider) {
                return ((SqlProvider) sqlProvider).getSql();
            }
            else {
                return null;
            }
        }
    
        private static boolean isTransactionalConnection(Connection connection, DataSource dataSource) {
            ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
            return conHolder != null && conHolder.getConnection() == connection;
        }
    
    }
    
    

    【讨论】:

      【解决方案2】:

      不要创建 jdbc 模板 bean。相反,您可以在每次需要执行查询时使用实体管理器工厂来创建 jdbc 模板的新实例。 这种方法对我有用。

      public class JdbcQueryTemplate {
      
          public JdbcTemplate getJdbcTemplate(EntityManagerFactory emf) {
              EntityManagerFactoryInfo info = (EntityManagerFactoryInfo) emf;
              return new JdbcTemplate(info.getDataSource());
          }
      
          public NamedParameterJdbcTemplate getNamedJdbcTemplate(EntityManagerFactory emf) {
              EntityManagerFactoryInfo info = (EntityManagerFactoryInfo) emf;
              return new NamedParameterJdbcTemplate(info.getDataSource());
          }
      }
      

      然后使用类进行查询。

      public class Test{
      
        @Autowired
        private EntityManagerFactory entityManagerFactory;
      
        public List<Entity> executeQuery() {
            return new JdbcQueryTemplate().getNamedJdbcTemplate(entityManagerFactory)
                    .query("query", new BeanPropertyRowMapper<>(Entity.class));
        }
      }
      

      【讨论】:

      • 解决方案工作正常,但导致连接过多错误。
      【解决方案3】:

      JdbcTemplate 不会有简单的方法来做到这一点,因为某些方法非常通用,例如execute(ConnectionCallback&lt;T&gt; action) 方法允许直接访问 java.sql.Connection 对象。

      为每个租户拥有一个单独的DataSource bean 并在 Spring 中使用合格的自动装配来解决这个问题会更容易。

      拥有每个租户 java.sql.Connection 将允许在打开新数据库连接时执行 USE tenantIdentifier 语句(一些池库支持这一点)。正如MySQL USE statement docs,每个会话可以执行一次:

      USE db_name 语句告诉 MySQL 使用 db_name 数据库作为后续语句的默认(当前)数据库。在会话结束或发出另一个 USE 语句之前,数据库将保持默认状态。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-11-21
        • 2021-02-15
        • 2023-03-17
        • 1970-01-01
        • 1970-01-01
        • 2020-04-26
        • 2011-08-05
        • 1970-01-01
        相关资源
        最近更新 更多