是否可以将null参数传递给Java JPA 2.1中的存储过程?

时间:2014-02-26 15:18:06

标签: java jpa stored-procedures

使用新的JPA 2.1 stored procedure调用,有没有办法传递空参数?

以下是一个示例用法:

StoredProcedureQuery storedProcedure = em.createStoredProcedureQuery("get_item", Item.class);
storedProcedure.registerStoredProcedureParameter(0, String.class, ParameterMode.IN);
storedProcedure.registerStoredProcedureParameter(1, String.class, ParameterMode.IN);
storedProcedure.registerStoredProcedureParameter(2, Timestamp.class, ParameterMode.IN);

storedProcedure.setParameter(0, a);
storedProcedure.setParameter(1, b);
storedProcedure.setParameter(2, c);

storedProcedure.execute();

当给出所有参数时,这是有效的,但当cnull时,它将因(PostgreSQL)JDBC驱动程序中的错误而失败。

Caused by: org.postgresql.util.PSQLException: No value specified for parameter 2
at org.postgresql.core.v3.SimpleParameterList.checkAllParametersSet(SimpleParameterList.java:216) [postgresql-9.3-1101.jdbc41.jar:]
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:244) [postgresql-9.3-1101.jdbc41.jar:]
    at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:559) [postgresql-9.3-1101.jdbc41.jar:]
    at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:417) [postgresql-9.3-1101.jdbc41.jar:]
    at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:410) [postgresql-9.3-1101.jdbc41.jar:]
    at org.jboss.jca.adapters.jdbc.WrappedPreparedStatement.execute(WrappedPreparedStatement.java:404)
    at org.hibernate.result.internal.OutputsImpl.<init>(OutputsImpl.java:69) [hibernate-core-4.3.1.Final.jar:4.3.1.Final]
    ... 244 more

我还考虑使用自己的类来传递参数,例如:

storedProcedure.registerStoredProcedureParameter(0, InputParameters.class, ParameterMode.IN);
storedProcedure.setParameter(0, inputParameters);

这失败了:

Caused by: java.lang.IllegalArgumentException: Type cannot be null

我猜是因为它需要一个可以映射到SQL类型的类型。

有没有办法传递空参数?

13 个答案:

答案 0 :(得分:29)

是的,使用JPA时可以将null参数传递给存储过程 StoredProcedureQuery。

您必须在application.properties文件中添加以下属性,并使用其名称注册参数。

spring.jpa.properties.hibernate.proc.param_null_passing=true

示例:

StoredProcedureQuery q = em.createStoredProcedureQuery(Globals.SPROC_PROBLEM_COMMENT2, ProblemCommentVO.class);
q.registerStoredProcedureParameter("Patient_ID", Long.class, ParameterMode.IN);
q.registerStoredProcedureParameter("Param2", Long.class, ParameterMode.IN);
q.registerStoredProcedureParameter("Param3", Long.class, ParameterMode.IN);
q.registerStoredProcedureParameter("Param4", Integer.class, ParameterMode.OUT);
q.setParameter("Patient_ID", patientId);
q.setParameter("Param2", null);//passing null value to Param2
q.setParameter("Param3", null);

List<ProblemCommentVO> pComments = q.getResultList();
Integer a = (Integer) q.getOutputParameterValue("Param4");

答案 1 :(得分:8)

以下是我对 JPA 2.1 相关的 Hibernate 4.3 的调查结果。

如果数据库不支持默认参数,则会抛出异常:

ProcedureCall procedure = getSession().createStoredProcedureCall("my_procedure");

procedure.registerParameter("my_nullable_param", String.class, ParameterMode.IN)
         .bindValue(null);

// execute
procedure.getOutputs();

Hibernate的源将参数绑定到基础CallableStatement

public abstract class AbstractParameterRegistrationImpl {

  ..

  @Override
  public void prepare(CallableStatement statement, int startIndex) throws SQLException {

    if ( mode == ParameterMode.INOUT || mode == ParameterMode.IN ) {
      if ( bind == null || bind.getValue() == null ) {
        // the user did not bind a value to the parameter being processed.  That might be ok *if* the
        // procedure as defined in the database defines a default value for that parameter.
        // Unfortunately there is not a way to reliably know through JDBC metadata whether a procedure
        // parameter defines a default value.  So we simply allow the procedure execution to happen
        // assuming that the database will complain appropriately if not setting the given parameter
        // bind value is an error.
        log.debugf("Stored procedure [%s] IN/INOUT parameter [%s] not bound; assuming procedure defines default value", procedureCall.getProcedureName(), this);
      } else {
         typeToUse.nullSafeSet( statement, bind.getValue(), startIndex, session() );
      }
    }
  }

  ..
}

以上评论内容如下:

  

用户未将值绑定到正在处理的参数。那   可能没问题如果数据库中定义的过程定义了一个   该参数的默认值。不幸的是,没有办法   通过JDBC元数据可靠地知道是否有过程参数   定义默认值。所以我们只是允许程序执行   假设数据库会适当地抱怨,如果发生   不设置给定参数绑定值是一个错误。

我正在解释为JPA(特别是Hibernate)根本不支持设置null参数。看起来他们正在努力支持默认参数值,而不是在适当时替换空值。他们选择支持前者。看起来那些需要支持后者(可空值)的人必须使用java.sql.CallableStatement

getSession().doWork(new Work() {

  @Override
  public void execute(Connection conn) throws SQLException {

    CallableStatement stmt = conn.prepareCall("{ call my_prodecure(:my_nullable_param) }");

    if(stringVariableThatIsNull != null) {
       stmt.setString("my_nullable_param", stringVariableThatIsNull);
    } else {
       stmt.setNull("my_nullable_param", Types.VARCHAR);
    }

    stmt.execute();
    stmt.close();

  }    
});

tl; dr 我们仍然被迫处理低级JDBC,因为JPA或Hibernate似乎都没有解决可以为空的参数。它们支持过程参数默认值而不是替换空值。

答案 2 :(得分:3)

我遇到了同样的问题。我无法控制存储过程,因此无法修改它以接受默认值。

但是,因为我使用的数据库是Oracle数据库,所以我能够通过将我的存储过程参数的数据类型(在orm.xml中)更改为java.lang.String然后替换来解决此问题一个空的String""),使用NULL值是合适的。由于Oracle以相同的方式处理空String s("")和NULL,因此我能够有效地欺骗Hibernate将“null”值传递给Oracle存储过程。

如果您不想使用编写低级JDBC代码,那么您的存储过程不可修改,并且您的数据库是基于Oracle的,请尝试这种方法。只需确保所有实体管理器代码都将参数视为预期的数据类型(在我的情况下为java.math.BigDecimal),并等到您将参数值设置为转换为java.lang.String

例如:

  

<强> orm.xml中:

<named-stored-procedure-query name="storedProcName" procedure-name="STORED_PROC_PACKAGE.STORED_PROC_NAME">
    <!-- String so that empty string can be used for NULL value by Hibernate JPA. -->
    <parameter name="my_nullable_in_param" mode="IN" class="java.lang.String" />

    <!-- was:
    <parameter name="my_nullable_in_param" mode="IN" class="java.math.BigDecimal" />
    -->

    <!-- other IN and OUT params -->
</named-stored-procedure-query>
     

<强>爪哇:

public void callStoredProc(java.math.BigDecimal myNullableInParam) {
    EntityManager entityManager = EMF.createEntityManager();

    StoredProcedureQuery query = entityManager.createNamedStoredProcedureQuery("storedProcName");

    query.setParameter("my_nullable_in_param", (myNullableInParam == null ? "" : myNullableInParam.toString()));

    // set more parameters, execute query, commit transaction, etc.
}

答案 3 :(得分:3)

设置属性 hibernate.proc.param_null_passing =真

示例:

small_icon

答案 4 :(得分:2)

要解决这个问题,如果我们没有很多可能的空参数,我们可以将多个@NamedStoredProcedureQuery映射到DB中的相同sproc。然后我们可以准备适当的查询。

例如,

  @NamedStoredProcedureQuery(name = "MyEntity.GetWithoutNullParam", procedureName = "dbo.GetProc", parameters = {
            @StoredProcedureParameter(mode = ParameterMode.IN, name = "id", type = Integer.class)},
            resultClasses = MyEntity.class),
    @NamedStoredProcedureQuery(name = "MyEntity.GetWithNullParam", procedureName = "dbo.GetProc", parameters = {
            @StoredProcedureParameter(mode = ParameterMode.IN, name = "id", type = Integer.class),
            @StoredProcedureParameter(mode = ParameterMode.IN, name = "nullableparam", type = String.class), 
resultClasses = MyEntity.class)

现在对&#34; nullableparam&#34;进行空检查。我们可以在存储库中创建适当的命名proc。类似于

的东西
if(MyEntity.nullableparam == null){StoredProcedureQuery storedProcedureQuery = em.createNamedStoredProcedureQuery("MyEntity.GetWithoutNullParam");}
else{StoredProcedureQuery storedProcedureQuery = em.createNamedStoredProcedureQuery("MyEntity.GetWithNullParam");}

答案 5 :(得分:2)

这是执行查询的方法,在Postgresql中使用setNull(i,Type.NULL)的“解决方法”。

解决方案是捕获特定异常并遍历所有可能类型的“java.sql.Types”,直到找到一些不会抛出异常的类型

private Connection rawConnection;
public ResultSet executeQuery(String sql, ArrayList<Object> params) throws SQLException
{
    PreparedStatement stmt = null;
    Iterator<Object> it;
    Object itemParam;
    ResultSet rs = null;
    int i = 1;

    Log.core.debug("Execute query sql:\n" + sql);

    stmt = rawConnection.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);

    if ( params != null && params.size() > 0 )
    {
        it = params.iterator();

        while ( it.hasNext() )
        {
            itemParam = it.next();

            if      ( itemParam == null )                   { stmt.setNull(i, Types.NULL); }
            else if ( itemParam instanceof String )         { stmt.setString(i, (String)itemParam); }
            else if ( itemParam instanceof Boolean )        { stmt.setBoolean(i, (Boolean)itemParam); }
            else if ( itemParam instanceof Integer )        { stmt.setInt(i, (Integer)itemParam); }
            else if ( itemParam instanceof Long )           { stmt.setLong(i, (Long)itemParam); }
            else if ( itemParam instanceof Float )          { stmt.setFloat(i, (Float)itemParam); }
            else if ( itemParam instanceof Double )         { stmt.setDouble(i, (Double)itemParam); }
            else if ( itemParam instanceof BigDecimal )     { stmt.setBigDecimal(i, (BigDecimal)itemParam); }

            else if ( itemParam instanceof Object[] ) // for postgresql, adapt for other dbs ??
            {
                Class<?> type = itemParam.getClass().getComponentType();

                if      ( type.equals(String.class) )       { stmt.setArray(i, rawConnection.createArrayOf("varchar", (Object[]) itemParam)); }
                else if ( type.equals(Boolean.class) )      { stmt.setArray(i, rawConnection.createArrayOf("bool", (Object[]) itemParam)); }
                else if ( type.equals(Integer.class) )      { stmt.setArray(i, rawConnection.createArrayOf("int4", (Object[]) itemParam)); }
                else if ( type.equals(Long.class) )         { stmt.setArray(i, rawConnection.createArrayOf("int8", (Object[]) itemParam)); }
                else if ( type.equals(Float.class) )        { stmt.setArray(i, rawConnection.createArrayOf("float4", (Object[]) itemParam)); }
                else if ( type.equals(Double.class) )       { stmt.setArray(i, rawConnection.createArrayOf("float8", (Object[]) itemParam)); }
                else if ( type.equals(BigDecimal.class) )   { stmt.setArray(i, rawConnection.createArrayOf("numeric", (Object[]) itemParam)); }
            }

            i++;
        }
    }

    infinite_loop:
    while ( true ) // fix for postgresql --> stmt.setNull(i, Types.NULL); // it's required set DataType in NULLs
    {
        try
        {
            rs =  stmt.executeQuery();
            break infinite_loop;
        }
        catch (SQLException e)
        {
            // fix for postgresql --> stmt.setNull(i, Types.NULL); // it's required set DataType in NULLs
            if ( IS_POSTGRESQL ) // adapt for other dbs ??
            {
                String regexParNumber = "\\$([0-9]+)", sqlState = "42P18";
                PSQLException pe = ((PSQLException)e), pe1;
                Pattern r, r1;
                Matcher m, m1;
                Field[] types;
                int paramNumber;

                if ( pe.getErrorCode() == 0 && sqlState.equals(pe.getSQLState()) )
                {
                    r = Pattern.compile(regexParNumber);
                    m = r.matcher(pe.getMessage());

                    if ( m.find() )
                    {
                        paramNumber = Integer.parseInt(m.group(1));
                        types = Types.class.getDeclaredFields();

                        Log.core.trace("Fix type for null sql argument[" + paramNumber + "] ...");

                        for ( Field type : types )
                        {
                            if ( !"NULL".equals(type.getName()) )
                            {
                                Log.core.trace("Fix type for null sql argument[" + paramNumber + "], trying type '" + type.getName() + "' ...");

                                try { stmt.setNull(paramNumber, type.getInt(null)); }
                                catch (IllegalArgumentException | IllegalAccessException e1) { continue; }

                                try
                                {
                                    rs =  stmt.executeQuery();
                                    break infinite_loop;
                                }
                                catch (SQLException e1)
                                {
                                    pe1 = ((PSQLException)e1);

                                    if ( pe1.getErrorCode() == 0 && sqlState.equals(pe1.getSQLState()) )
                                    {
                                        r1 = Pattern.compile(regexParNumber);
                                        m1 = r1.matcher(pe1.getMessage());

                                        if ( m1.find() )
                                        {
                                            if ( paramNumber == Integer.parseInt(m1.group(1)) ) { continue; }
                                            else { continue infinite_loop; }
                                        }                                       
                                    }

                                    throw e1;
                                }
                            }
                        }
                    }
                }
            }

            throw e;
        }
        finally
        {   
            SQLWarning warns;
            Iterator<Throwable> itWarn;
            Throwable raise;

            try
            {
                warns = stmt.getWarnings();

                if ( warns != null )
                {
                    itWarn = warns.iterator();

                    while ( itWarn.hasNext() )
                    {
                        raise = itWarn.next();
                        Log.core.debug("<DbMain Log> --> " + raise.getMessage());
                    }
                }
            }
            catch (SQLException e1) {}
        }
    }

    return rs;
}

答案 6 :(得分:2)

以下步骤解决了问题:

在springboot hibernate环境中的属性文件中设置全局参数 spring.jpa.properties.hibernate.proc.param_null_passing =真

允许存储过程在hibernate java代码中传递null。

答案 7 :(得分:1)

这对我有用

if (columnname.length() > 0) {
    fSignUp.setString(3, columnname); 
} else {
    fSignUp.setNull(<position>, Types.NULL);
}

答案 8 :(得分:0)

您可以创建一种方法来强制启用所有调用参数,如下面的代码所示,如​​果您的StoredProcedureQuery包含out参数,该方法也可以正常工作。这可以帮助您在某处不会破坏null参数检查。

public void setStoreProcedureEnableNullParameters(StoredProcedureQuery storedProcedureQuery) {
    if (storedProcedureQuery == null || storedProcedureQuery.getParameters() == null)
        return;

    for (Parameter parameter : storedProcedureQuery.getParameters()) {
        ((ProcedureParameterImpl) parameter).enablePassingNulls(true);
    }
}

然后打电话

StoredProcedureQuery storedProcedure = entityManager.createStoredProcedureQuery("your_store_procedure")
    .registerStoredProcedureParameter("Param_1", String.class, ParameterMode.OUT)
    .registerStoredProcedureParameter("Param_2", String.class, ParameterMode.IN)
    .registerStoredProcedureParameter("Param_3", String.class, ParameterMode.IN);

// Remember you must call before setting the value for the parameters
setStoreProcedureEnableNullParameters(storedProcedure);

storedProcedure
    .setParameter("Param_2", null)
    .setParameter("Param_3", "Some value");
storedProcedure.execute();

String outValue = (String) storedProcedure.getOutputParameterValue("Param_1");

答案 9 :(得分:0)

简单的回答是,使用JPA StoredProcedureQuery时可以将空参数传递给存储过程。

有几种方法可以实现此目的。主要解决方案取决于您使用的休眠版本。

休眠4.3 +

Session session = em.unwrap(Session.class);
ProcedureCall procedure = session.createStoredProcedureCall(Constants.MY_PROCEDURE_NAME);

q.registerStoredProcedureParameter("Patient_ID", Long.class, ParameterMode.IN).enablePassingNulls(true);
q.registerStoredProcedureParameter("Param2", String.class, ParameterMode.IN).enablePassingNulls(true);
q.registerStoredProcedureParameter("Param3", Long.class, ParameterMode.IN).enablePassingNulls(true);
q.registerStoredProcedureParameter("Param4", Integer.class, ParameterMode.OUT);

q.setParameter("Param1", patientId);
q.setParameter("Param2", null);//passing null value to Param2
q.setParameter("Param3", null);//passing null value to Param3

List<> results = q.getResultList();
Integer a = (Integer) q.getOutputParameterValue("Param4");

旧版本

您必须在application.properties文件中添加以下属性,并使用其名称注册参数。

spring.jpa.properties.hibernate.proc.param_null_passing=true

示例:

StoredProcedureQuery q = em.createStoredProcedureQuery(Constants.MY_PROCEDURE_NAME, ResultData.class);
q.registerStoredProcedureParameter("Patient_ID", Long.class, ParameterMode.IN);
q.registerStoredProcedureParameter("Param2", String.class, ParameterMode.IN);
q.registerStoredProcedureParameter("Param3", Long.class, ParameterMode.IN);
q.registerStoredProcedureParameter("Param4", Integer.class, ParameterMode.OUT);
q.setParameter("Param1", patientId);
q.setParameter("Param2", null);//passing null value to Param2
q.setParameter("Param3", null);//passing null value to Param3

List<> results = q.getResultList();
Integer a = (Integer) q.getOutputParameterValue("Param4");

答案 10 :(得分:0)

将以下属性添加到您的配置持久性

hibernate.proc.param_null_passing=true

答案 11 :(得分:0)

使用spring.jpa.properties.hibernate.proc.param_null_passing=true定义存储过程查询时,

@NamedStoredProcedureQuery不起作用。在这种情况下,输入空值的唯一方法是将可选参数定义为INOUT而不是IN。示例:

@NamedStoredProcedureQueries({
        @NamedStoredProcedureQuery(name = "nameOfStoredProcedureCallingJpaMethod",
            procedureName = "nameOfStoredProcedure",
            parameters = {
                        @StoredProcedureParameter(name = "someParameter", type = String.class, mode = ParameterMode.IN),
                        @StoredProcedureParameter(name = "myOptionalParameter", type = String.class, mode = ParameterMode.INOUT)
                })
)

答案 12 :(得分:-1)

我们可以使用Empty字符串,它也可以作为null。 例如。从双重选择NVL('','XXX');返回XXX 尝试将StringUtils.EMPTY作为参数传递。