Consulta jdbctemplate para la cadena: EmptyResultDataAccessException: Tamaño de resultado incorrecto: esperado 1, real 0

105

Estoy usando Jdbctemplate para recuperar un solo valor de cadena de la base de datos. Este es mi método.

    public String test() {
        String cert=null;
        String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN 
             where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
        cert = (String) jdbc.queryForObject(sql, String.class); 
        return cert;
    }

En mi escenario, es completamente posible NO obtener un resultado en mi consulta, por lo que mi pregunta es cómo puedo solucionar el siguiente mensaje de error.

EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0

Me parece que debería obtener un valor nulo en lugar de lanzar una excepción. ¿Cómo puedo arreglar esto? Gracias por adelantado.

Byron
fuente

Respuestas:

179

En JdbcTemplate, queryForInt, queryForLong, queryForObjecttodos los métodos tales espera que consulta ejecutada volverán una y sólo una fila. Si no obtiene filas o más de una, se obtendrá como resultado IncorrectResultSizeDataAccessException. Ahora, la forma correcta es no detectar esta excepción o EmptyResultDataAccessException, pero asegúrese de que la consulta que está utilizando devuelva solo una fila. Si no es posible, utilice el querymétodo en su lugar.

List<String> strLst  = getJdbcTemplate().query(sql,new RowMapper {

  public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        return rs.getString(1);
  }

});

if ( strLst.isEmpty() ){
  return null;
}else if ( strLst.size() == 1 ) { // list contains exactly 1 element
  return strLst.get(0);
}else{  // list contains more than 1 elements
  //your wish, you can either throw the exception or return 1st element.    
}
Rakesh Juyal
fuente
Como se menciona a continuación, el único inconveniente aquí es que si el tipo de retorno fuera un tipo complejo, estaría creando varios objetos y creando una lista, también ResultSet.next()se llamaría innecesariamente. El uso de a ResultSetExtractores una herramienta mucho más eficiente en este caso.
Brett Ryan
3
Falta un paréntesis en la definición de clase anónima - nuevo RowMapper ()
Janis Koluzs
Estoy con Brett en esto. ResultSetExtractor es más limpio :)
laher
2
Hola @Rakesh, ¿por qué no solo return nullen catch(EmptyResultDataAccessException exception){ return null; }?
Vishal Zanzrukia
1
¡Oye! ¿Puedo preguntar por qué 'Ahora la forma correcta es no detectar esta excepción', considerando si está utilizando queryForObject? ¿Qué sería de malo detectar una excepción en el caso de queryForObject? Gracias :)
Michael Stokes
48

También puede utilizar a en ResultSetExtractorlugar de a RowMapper. Ambos son tan fáciles como el otro, la única diferencia es tu llamada ResultSet.next().

public String test() {
    String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN "
                 + " where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
    return jdbc.query(sql, new ResultSetExtractor<String>() {
        @Override
        public String extractData(ResultSet rs) throws SQLException,
                                                       DataAccessException {
            return rs.next() ? rs.getString("ID_NMB_SRZ") : null;
        }
    });
}

El ResultSetExtractortiene la ventaja añadida de que puede manejar todos los casos en los que hay más de una fila o no hay filas devueltas.

ACTUALIZACIÓN : Varios años después y tengo algunos trucos para compartir. JdbcTemplatefunciona magníficamente con java 8 lambdas para las que están diseñados los siguientes ejemplos, pero puede usar fácilmente una clase estática para lograr lo mismo.

Si bien la pregunta es sobre tipos simples, estos ejemplos sirven como guía para el caso común de extracción de objetos de dominio.

Antes que nada. Supongamos que tiene un objeto de cuenta con dos propiedades para simplificar Account(Long id, String name). Es probable que desee tener un RowMapperobjeto para este dominio.

private static final RowMapper<Account> MAPPER_ACCOUNT =
        (rs, i) -> new Account(rs.getLong("ID"),
                               rs.getString("NAME"));

Ahora puede usar este mapeador directamente dentro de un método para mapear Accountobjetos de dominio desde una consulta ( jtes una JdbcTemplateinstancia).

public List<Account> getAccounts() {
    return jt.query(SELECT_ACCOUNT, MAPPER_ACCOUNT);
}

Genial, pero ahora queremos nuestro problema original y usamos mi solución original reutilizando el RowMapperpara realizar el mapeo por nosotros.

public Account getAccount(long id) {
    return jt.query(
            SELECT_ACCOUNT,
            rs -> rs.next() ? MAPPER_ACCOUNT.mapRow(rs, 1) : null,
            id);
}

Genial, pero este es un patrón que puede y querrá repetir. Por lo tanto, puede crear un método de fábrica genérico para crear uno nuevo ResultSetExtractorpara la tarea.

public static <T> ResultSetExtractor singletonExtractor(
        RowMapper<? extends T> mapper) {
    return rs -> rs.next() ? mapper.mapRow(rs, 1) : null;
}

Crear un ResultSetExtractorahora se vuelve trivial.

private static final ResultSetExtractor<Account> EXTRACTOR_ACCOUNT =
        singletonExtractor(MAPPER_ACCOUNT);

public Account getAccount(long id) {
    return jt.query(SELECT_ACCOUNT, EXTRACTOR_ACCOUNT, id);
}

Espero que esto ayude a demostrar que ahora puede combinar partes con bastante facilidad de una manera poderosa para simplificar su dominio.

ACTUALIZACIÓN 2 : Combine con un Opcional para valores opcionales en lugar de nulos.

public static <T> ResultSetExtractor<Optional<T>> singletonOptionalExtractor(
        RowMapper<? extends T> mapper) {
    return rs -> rs.next() ? Optional.of(mapper.mapRow(rs, 1)) : Optional.empty();
}

Que ahora cuando se usa podría tener lo siguiente:

private static final ResultSetExtractor<Optional<Double>> EXTRACTOR_DISCOUNT =
        singletonOptionalExtractor(MAPPER_DISCOUNT);

public double getDiscount(long accountId) {
    return jt.query(SELECT_DISCOUNT, EXTRACTOR_DISCOUNT, accountId)
            .orElse(0.0);
}
Brett Ryan
fuente
21

Esa no es una buena solución porque depende de las excepciones para controlar el flujo. En su solución, es normal obtener excepciones, es normal tenerlas en el registro.

public String test() {
    String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
    List<String> certs = jdbc.queryForList(sql, String.class); 
    if (certs.isEmpty()) {
        return null;
    } else {
        return certs.get(0);
    }
}
Philippe Marschall
fuente
Puede que mi solución no sea la más elegante, pero al menos la mía funciona. Da un ejemplo de queryForObjectList que ni siquiera es una opción con Jdbctemplate.
Byron
1
El único inconveniente aquí es que si el tipo de retorno fuera un tipo complejo, estaría creando varios objetos y creando una lista, también ResultSet.next()se llamaría innecesariamente. El uso de a ResultSetExtractores una herramienta mucho más eficiente en este caso.
Brett Ryan
¿Y si no tener valor es una opción, pero no tener más de uno? Tengo este patrón a menudo y me gustaría tener un queryForOptionalObject en Spring para este propósito.
Guillaume
7

De hecho, puedes jugar JdbcTemplatey personalizar tu propio método como prefieras. Mi sugerencia es hacer algo como esto:

public String test() {
    String cert = null;
    String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN
        where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
    ArrayList<String> certList = (ArrayList<String>) jdbc.query(
        sql, new RowMapperResultSetExtractor(new UserMapper()));
    cert =  DataAccessUtils.singleResult(certList);

    return cert;
}

Funciona como el original jdbc.queryForObject, pero sin throw new EmptyResultDataAccessExceptioncuándo size == 0.

Alex
fuente
@Abdull UserMapper implements RowMapper<String>.
Brett Ryan
Creo que es la mejor respuesta aquí, ya que ofrece la sintaxis más corta
Stan Sokolov
DataAccessUtils.singleResult(...)es lo que estaba buscando. Thx
Drakes
7

Dado que devolver un valor nulo cuando no hay datos es algo que quiero hacer a menudo cuando uso queryForObject, me ha resultado útil extender JdbcTemplate y agregar un método queryForNullableObject similar al siguiente.

public class JdbcTemplateExtended extends JdbcTemplate {

    public JdbcTemplateExtended(DataSource datasource){
        super(datasource);
    }

    public <T> T queryForNullableObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        List<T> results = query(sql, rowMapper);

        if (results == null || results.isEmpty()) {
            return null;
        }
        else if (results.size() > 1) {
            throw new IncorrectResultSizeDataAccessException(1, results.size());
        }
        else{
            return results.iterator().next();
        }
    }

    public <T> T queryForNullableObject(String sql, Class<T> requiredType) throws DataAccessException {
        return queryForObject(sql, getSingleColumnRowMapper(requiredType));
    }

}

Ahora puede usar esto en su código de la misma manera que usó queryForObject

String result = queryForNullableObject(queryString, String.class);

Me interesaría saber si alguien más piensa que esta es una buena idea.

Stewart Evans
fuente
1
Lo es, y debería ser en primavera
Guillaume
6

Ok, lo descubrí. Simplemente lo envolví en un intento de captura y devolví nulo.

    public String test() {
            String cert=null;
            String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN 
                     where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
            try {
                Object o = (String) jdbc.queryForObject(sql, String.class);
                cert = (String) o;
            } catch (EmptyResultDataAccessException e) {
                e.printStackTrace();
            }
            return cert;
    }
Byron
fuente
1
No veo por qué esto es tan malo y por qué recibió tanto voto negativo por ello, además de ser un fundamentalista en el principio de "ningún programa fluye dentro de la excepción". Simplemente habría reemplazado el rastro de la pila de impresión con un comentario que explicara el caso y no haría nada más.
Guillaume
4

Con Java 8 o superior, puede utilizar un Optionaly Java Streams.

Entonces, simplemente puede usar el JdbcTemplate.queryForList()método, crear un Stream y usar el Stream.findFirst()que devolverá el primer valor del Stream o un vacío Optional:

public Optional<String> test() {
    String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
    return jdbc.queryForList(sql, String.class)
            .stream().findFirst();
}

Para mejorar el rendimiento de la consulta, puede agregar LIMIT 1a su consulta, de modo que no se transfiera más de 1 elemento de la base de datos.

Samuel Philipp
fuente
1
Bonito y limpio. Sin ifs ni lambdas adicionales. Me gusta.
BeshEater
2

Puede utilizar una función de grupo para que su consulta siempre devuelva un resultado. es decir

MIN(ID_NMB_SRZ)
DS.
fuente
1

En Postgres, puede hacer que casi cualquier consulta de valor único devuelva un valor o nulo envolviéndola:

SELECT (SELECT <query>) AS value

y por lo tanto evitar la complejidad en la persona que llama.

Rico
fuente
1

Desde getJdbcTemplate (). QueryForMap espera un tamaño mínimo de uno, pero cuando devuelve un valor nulo, muestra EmptyResultDataAccesso fix dis cuando se puede usar a continuación la lógica

Map<String, String> loginMap =null;
try{
    loginMap = getJdbcTemplate().queryForMap(sql, new Object[] {CustomerLogInInfo.getCustLogInEmail()});
}
catch(EmptyResultDataAccessException ex){
    System.out.println("Exception.......");
    loginMap =null;
}
if(loginMap==null || loginMap.isEmpty()){
    return null;
}
else{
    return loginMap;
}
Mahesh Jayachandran
fuente
0

Me ocupé de esto antes y había publicado en los foros de primavera.

http://forum.spring.io/forum/spring-projects/data/123129-frustrated-with-emptyresultdataaccessexception

El consejo que recibimos fue utilizar un tipo de SQlQuery. A continuación, se muestra un ejemplo de lo que hicimos al intentar obtener un valor de una base de datos que podría no estar allí.

@Component
public class FindID extends MappingSqlQuery<Long> {

        @Autowired
        public void setDataSource(DataSource dataSource) {

                String sql = "Select id from address where id = ?";

                super.setDataSource(dataSource);

                super.declareParameter(new SqlParameter(Types.VARCHAR));

                super.setSql(sql);

                compile();
        }

        @Override
        protected Long mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getLong(1);
        }

En el DAO, simplemente llamamos ...

Long id = findID.findObject(id);

No está claro en cuanto al rendimiento, pero funciona y está limpio.

grbonk
fuente
0

Para Byron, puedes probar esto ...

public String test(){
                String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN 
                     where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
                List<String> li = jdbcTemplate.queryForList(sql,String.class);
                return li.get(0).toString();
        }
Mohan Kumar Dg
fuente
0

para hacer

    jdbcTemplate.queryForList(sql, String.class)

funciona, asegúrese de que su jdbcTemplate sea de tipo

    org.springframework.jdbc.core.JdbcTemplate
Dmitry
fuente
0

Podemos usar query en lugar de queryForObject, la principal diferencia entre query y queryForObject es que la lista de retorno de la consulta de Object (basada en el tipo de retorno del asignador de filas) y esa lista puede estar vacía si no se reciben datos de la base de datos, mientras que queryForObject siempre espera que solo un solo objeto sea obtenido de db ni filas nulas ni múltiples y, en caso de que el resultado esté vacío, queryForObject arroja EmptyResultDataAccessException, había escrito un código usando una consulta que superará el problema de EmptyResultDataAccessException en caso de resultado nulo.

----------


public UserInfo getUserInfo(String username, String password) {
      String sql = "SELECT firstname, lastname,address,city FROM users WHERE id=? and pass=?";
      List<UserInfo> userInfoList = jdbcTemplate.query(sql, new Object[] { username, password },
              new RowMapper<UserInfo>() {
                  public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
                      UserInfo user = new UserInfo();
                      user.setFirstName(rs.getString("firstname"));
                      user.setLastName(rs.getString("lastname"));
                      user.setAddress(rs.getString("address"));
                      user.setCity(rs.getString("city"));

                      return user;
                  }
              });

      if (userInfoList.isEmpty()) {
          return null;
      } else {
          return userInfoList.get(0);
      }
  }
ABHAY JOHRI
fuente
0

En mi humilde opinión, devolver un nulles una mala solución porque ahora tiene el problema de enviarlo e interpretarlo en el (probable) cliente de interfaz. Tuve el mismo error y lo resolví simplemente devolviendo un archivo List<FooObject>. Yo usé JDBCTemplate.query().

En el front-end (cliente web angular), simplemente examino la lista y si está vacía (de longitud cero), la trato como si no se encontraran registros.

likejudo
fuente
-1

Acabo de captar esta "EmptyResultDataAccessException"

public Myclass findOne(String id){
    try {
        Myclass m = this.jdbcTemplate.queryForObject(
                "SELECT * FROM tb_t WHERE id = ?",
                new Object[]{id},
                new RowMapper<Myclass>() {
                    public Myclass mapRow(ResultSet rs, int rowNum) throws SQLException {
                        Myclass m = new Myclass();
                        m.setName(rs.getString("name"));
                        return m;
                    }
                });
        return m;
    } catch (EmptyResultDataAccessException e) { // result.size() == 0;
        return null;
    }
}

entonces puedes comprobar:

if(m == null){
    // insert operation.
}else{
    // update operation.
}
Remolino
fuente
Podemos usar query en lugar de queryForObject
ABHAY JOHRI
1
Generalmente se considera una mala práctica abusar de excepciones como esta. Las excepciones no son para el flujo lógico del programa predecible, son para situaciones excepcionales.
Chris Baker