Spring SimpleJdbcCall 默认(可选)参数

Posted

技术标签:

【中文标题】Spring SimpleJdbcCall 默认(可选)参数【英文标题】:Spring SimpleJdbcCall default (optional) arguments 【发布时间】:2012-10-21 03:40:27 【问题描述】:

我正在尝试调用具有默认(可选)参数而不传递它们的存储过程,但它不起作用。与here 描述的问题基本相同。

我的代码:

  SqlParameterSource in = new MapSqlParameterSource()
        .addValue("ownname", "USER")
        .addValue("tabname", cachedTableName)
        .addValue("estimate_percent", 20)
        .addValue("method_opt", "FOR ALL COLUMNS SIZE 1")
        .addValue("degree", 0)
        .addValue("granularity", "AUTO")
        .addValue("cascade", Boolean.TRUE)
        .addValue("no_invalidate", Boolean.FALSE)
        .addValue("force", Boolean.FALSE);

我得到一个例外:

Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Required input parameter 'PARTNAME' is missing
    at org.springframework.jdbc.core.CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement(CallableStatementCreatorFactory.java:209)

其中 PARTNAME 是根据this 的可选参数。也证实了我可以在没有 PARTNAME 参数的情况下手动运行此过程。

【问题讨论】:

第二个超链接显示 404 。 【参考方案1】:

在放弃这个问题并只传递所有参数(包括可选参数)后,我遇到了它无法传递布尔参数的问题,因为布尔不是 SQL 数据类型,只有 PL/SQL。

所以我目前的解决方案是 JDBC 不适合运行存储过程,这就是我正在解决的问题:

  jdbcTemplate.execute(
        new CallableStatementCreator() 
           public CallableStatement createCallableStatement(Connection con) throws SQLException
              CallableStatement cs = con.prepareCall("call sys.dbms_stats.gather_table_stats(ownname=>user, tabname=>'" + cachedMetadataTableName + "', estimate_percent=>20, method_opt=>'FOR ALL COLUMNS SIZE 1', degree=>0, granularity=>'AUTO', cascade=>TRUE, no_invalidate=>FALSE, force=>FALSE) ");
              return cs;
           
        ,
        new CallableStatementCallback() 
           public Object doInCallableStatement(CallableStatement cs) throws SQLException
              cs.execute();
              return null; // Whatever is returned here is returned from the jdbcTemplate.execute method
           
        
  );

【讨论】:

【参考方案2】:

也在努力解决这个问题,不想处理字符串。 如果我们从元数据中获取默认值,可能会有更有趣的解决方案,spring 在默认实现中并不关心,但我只是将空值放在那里。 解决方案如下:

重写的 simpleJdbcCall

 private class JdbcCallWithDefaultArgs extends SimpleJdbcCall 

    CallableStatementCreatorFactory callableStatementFactory;

    public JdbcCallWithDefaultArgs(JdbcTemplate jdbcTemplate) 
        super(jdbcTemplate);
    

    @Override
    protected CallableStatementCreatorFactory getCallableStatementFactory() 
        return callableStatementFactory;
    

    @Override
    protected void onCompileInternal() 
        callableStatementFactory =
                new CallableStatementCreatorWithDefaultArgsFactory(getCallString(), this.getCallParameters());
        callableStatementFactory.setNativeJdbcExtractor(getJdbcTemplate().getNativeJdbcExtractor());

    


    @Override
    public Map<String, Object> execute(SqlParameterSource parameterSource) 
        ((CallableStatementCreatorWithDefaultArgsFactory)callableStatementFactory).cleanupParameters(parameterSource);
        return super.doExecute(parameterSource);
    

并重写 CallableStatementCreatorFactory

public class CallableStatementCreatorWithDefaultArgsFactory extends CallableStatementCreatorFactory 

private final Logger logger = LoggerFactory.getLogger(getClass());
private final List<SqlParameter> declaredParameters;

public CallableStatementCreatorWithDefaultArgsFactory(String callString, List<SqlParameter> declaredParameters) 
    super(callString, declaredParameters);
    this.declaredParameters = declaredParameters;


protected void cleanupParameters(SqlParameterSource sqlParameterSource) 
    MapSqlParameterSource mapSqlParameterSource = (MapSqlParameterSource) sqlParameterSource;
    Iterator<SqlParameter> declaredParameterIterator = declaredParameters.iterator();
    Set<String> parameterNameSet = mapSqlParameterSource.getValues().keySet();
    while (declaredParameterIterator.hasNext()) 
        SqlParameter parameter = declaredParameterIterator.next();
        if (!(parameter instanceof SqlOutParameter) &&
                (!mapContainsParameterIgnoreCase(parameter.getName(), parameterNameSet))) 
            logger.warn("Missing value parameter "+parameter.getName() + " will be replaced by null!");
            mapSqlParameterSource.addValue(parameter.getName(), null);
        
    


private boolean mapContainsParameterIgnoreCase(String parameterName, Set<String> parameterNameSet) 
    String lowerParameterName = parameterName.toLowerCase();
    for (String parameter : parameterNameSet) 
        if (parameter.toLowerCase().equals(lowerParameterName)) 
            return true;
        
    
    return false;


@Override
public void addParameter(SqlParameter param) 
    this.declaredParameters.add(param);

【讨论】:

【参考方案3】:

这是我采用的另一种方法。我添加了让用户设置他们将在通话中提供的参数数量的功能。这些将是前 n 个位置参数。存储过程中可用的任何剩余参数都必须通过数据库的默认值处理进行设置。这允许使用默认值将新参数添加到列表的末尾,或者可以为空,而不会破坏不知道提供值的代码。

我对 SimpleJdbcCall 进行了子类化,并添加了设置“maxParamCount”的方法。我还使用了一点邪恶的反射来设置我的 CallMetaDataContext 的子类版本。

public class MySimpleJdbcCall extends SimpleJdbcCall

    private final MyCallMetaDataContext callMetaDataContext = new MyCallMetaDataContext();

    public MySimpleJdbcCall(DataSource dataSource)
    
        this(new JdbcTemplate(dataSource));
    

    public MySimpleJdbcCall(JdbcTemplate jdbcTemplate)
    
        super(jdbcTemplate);

        try
        
            // Access private field
            Field callMetaDataContextField = AbstractJdbcCall.class.getDeclaredField("callMetaDataContext");
            callMetaDataContextField.setAccessible(true);

            // Make it non-final
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(callMetaDataContextField, callMetaDataContextField.getModifiers() & ~Modifier.FINAL);

            // Set field
            callMetaDataContextField.set(this, this.callMetaDataContext);
        
        catch (NoSuchFieldException | IllegalAccessException ex)
        
            throw new RuntimeException("Exception thrown overriding AbstractJdbcCall.callMetaDataContext field", ex);
        
    

    public MySimpleJdbcCall withMaxParamCount(int maxInParamCount)
    
        setMaxParamCount(maxInParamCount);
        return this;
    

    public int getMaxParamCount()
    
        return this.callMetaDataContext.getMaxParamCount();
    

    public void setMaxParamCount(int maxInParamCount)
    
        this.callMetaDataContext.setMaxParamCount(maxInParamCount);
    

在我的 CallMetaDataContext 子类中,我存储 maxInParamCount,并使用它来修剪已知存在于存储过程中的参数列表。

public class MyCallMetaDataContext extends CallMetaDataContext

    private int maxParamCount = Integer.MAX_VALUE;

    public int getMaxParamCount()
    
        return maxParamCount;
    

    public void setMaxParamCount(int maxInParamCount)
    
        this.maxParamCount = maxInParamCount;
    

    @Override
    protected List<SqlParameter> reconcileParameters(List<SqlParameter> parameters)
    
        List<SqlParameter> limittedParams = new ArrayList<>();
        int paramCount = 0;
        for(SqlParameter param : super.reconcileParameters(parameters))
        
            if (!param.isResultsParameter())
            
                paramCount++;
                if (paramCount > this.maxParamCount)
                    continue;
            

            limittedParams.add(param);
        
        return limittedParams;
    

除了看到最大参数计数之外,使用基本相同。

SimpleJdbcCall call = new MySimpleJdbcCall(jdbcTemplate)
        .withMaxParamCount(3)
        .withProcedureName("MayProc");

SMALL RANT:有趣的是,Spring 以其 IOC 容器而闻名。但是,在它的实用程序类中,我必须借助反射来提供依赖类的替代实现。

【讨论】:

【参考方案4】:

今天想出了一个不错的解决方案,它可以处理非空默认值,并且不使用果味反射技术。它的工作原理是在外部为函数创建元数据上下文以检索所有参数类型等,然后从中手动构造 SimpleJdbcCall。

首先,为函数创建一个CallMetaDataContext:

    CallMetaDataContext context = new CallMetaDataContext();
    context.setFunction(true);
    context.setSchemaName(schemaName);
    context.setProcedureName(functionName);
    context.initializeMetaData(jdbcTemplate.getDataSource());
    context.processParameters(Collections.emptyList());

接下来,创建 SimpleJdbcCall,但强制它不进行自己的元数据查找:

SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate);
// This forces the call object to skip metadata lookup, which is the part that forces all parameters
simpleJdbcCall.setAccessCallParameterMetaData(false);

// Now go back to our previously created context and pull the parameters we need from it
simpleJdbcCall.addDeclaredParameter(context.getCallParameters().get(0));
for (int i = 0; i < params.length; ++i) 
    simpleJdbcCall.addDeclaredParameter(context.getCallParameters().get(i));

// Call the function and retrieve the result
Map<String, Object> resultsMap = simpleJdbcCall
                        .withSchemaName(schemaName)
                        .withFunctionName(functionName)
                        .execute(params);
Object returnValue = resultsMap.get(context.getScalarOutParameterName());

【讨论】:

【参考方案5】:

我使用这个 util 方法:

public <T> void setOptionalParameter(MapSqlParameterSource parameters, String name, T value) 
    if (value == null)
        parameters.addValue(name, value, Types.NULL);
    else
        parameters.addValue(name, value);

【讨论】:

【参考方案6】:

我找到了 SimpleJdbcCall 和 Spring 5.2.1、Java 8、Oracle 12 的解决方案。

你需要:

    使用 .withoutProcedureColumnMetaDataAccess() 使用 .withNamedBinding() 声明参数,你知道在 .declareParameters() 调用。仅使用在此方法中声明的参数调用过程。默认参数,不想设置,这里就不写了。

示例调用如下

final String dataParamName = "P_DATA";
final String ageParamName = "P_AGE";
final String genderParamName = "P_GENDER";

final String acceptedParamName = "P_ACCEPTED";


SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(getJdbcTemplate())
        .withCatalogName("PKG_USER")
        .withProcedureName("USER_CHECK")
        .withoutProcedureColumnMetaDataAccess()
        .withNamedBinding()
        .declareParameters(
                new SqlParameter(dataParamName, OracleTypes.VARCHAR),
                new SqlParameter(ageParamName, OracleTypes.NUMBER),
                new SqlParameter(genderParamName, OracleTypes.VARCHAR),
                new SqlOutParameter(acceptedParamName, OracleTypes.NUMBER)
        );

SqlParameterSource parameterSource = new MapSqlParameterSource()
        .addValue(dataParamName, data)
        .addValue(ageParamName, age)
        .addValue(genderParamName, gender);

Map<String, Object> out = simpleJdbcCall.execute(parameterSource);

【讨论】:

以上是关于Spring SimpleJdbcCall 默认(可选)参数的主要内容,如果未能解决你的问题,请参考以下文章

SimpleJdbcCall 不适用于存储的函数

如何在 spring 中使用 SimpleJDBCCall 获取存储过程的结果以及两个结果表?

从spring boot simpleJDBCcall调用PL SQL过程时出错

spring3: 对JDBC的支持 之 Spring提供的其它帮助 SimpleJdbcInsert/SimpleJdbcCall/SqlUpdate/JdbcTemplate 生成主键/批量处理(

SimpleJDBCCall 类参数传递

SimpleJdbcCall 找不到存储过程