Consulta Jdbctemplate para string: EmptyResultDataAccessException: Tamanho de resultado incorreto: esperado 1, real 0


105

Estou usando o Jdbctemplate para recuperar um único valor String do banco de dados. Aqui está meu 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;
    }

No meu cenário, é totalmente possível NÃO obter uma resposta em minha consulta, então minha pergunta é como faço para contornar a seguinte mensagem de erro.

EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0

Parece-me que devo apenas obter um valor nulo em vez de lançar uma exceção. Como posso consertar isso? Desde já, obrigado.

Respostas:


179

Em JdbcTemplate, queryForInt, queryForLong, queryForObjecttodos esses métodos espera que consulta executada irá retornar uma e apenas uma linha. Se você não obtiver nenhuma linha ou mais de uma linha, isso resultará em IncorrectResultSizeDataAccessException. Agora, a maneira correta é não capturar essa exceção ou EmptyResultDataAccessException, mas certificar-se de que a consulta que você está usando deve retornar apenas uma linha. Se isso não for possível, use o querymétodo.

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.    
}

Conforme mencionado abaixo, a única desvantagem aqui é que se o tipo de retorno fosse um tipo complexo, você estaria construindo vários objetos e instanciando uma lista, também ResultSet.next()seria chamado desnecessariamente. Usar a ResultSetExtractoré uma ferramenta muito mais eficiente neste caso.
Brett Ryan

3
Faltam parênteses na definição de classe anônima - novo RowMapper ()
Janis Koluzs

Estou com Brett nisso. ResultSetExtractor is mais limpo :)
laher

2
Olá @Rakesh, por que não apenas return nullem catch(EmptyResultDataAccessException exception){ return null; }?
Vishal Zanzrukia

1
Ei! Posso apenas perguntar por que 'Agora a maneira correta é não capturar essa exceção', considerando se você está usando queryForObject? O que haveria de errado em capturar uma exceção no caso de queryForObject? Obrigado :)
Michael Stokes

48

Você também pode usar um em ResultSetExtractorvez de a RowMapper. Ambos são tão fáceis um quanto o outro, a única diferença é você ligar 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;
        }
    });
}

O ResultSetExtractortem a vantagem adicional de poder lidar com todos os casos em que há mais de uma linha ou nenhuma linha retornada.

ATUALIZAÇÃO : Vários anos depois, tenho alguns truques para compartilhar. JdbcTemplatefunciona muito bem com java 8 lambdas, para os quais os exemplos a seguir foram projetados, mas você pode facilmente usar uma classe estática para fazer o mesmo.

Embora a questão seja sobre tipos simples, esses exemplos servem como um guia para o caso comum de extração de objetos de domínio.

Primeiramente. Vamos supor que você tenha um objeto de conta com duas propriedades para simplificar Account(Long id, String name). Você provavelmente gostaria de ter um RowMapperpara este objeto de domínio.

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

Agora você pode usar este mapeador diretamente em um método para mapear Accountobjetos de domínio de uma consulta ( jté uma JdbcTemplateinstância).

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

Ótimo, mas agora queremos nosso problema original e usamos minha solução original reutilizando o RowMapperpara realizar o mapeamento para nós.

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

Ótimo, mas esse é um padrão que você pode e deseja repetir. Portanto, você pode criar um método de fábrica genérico para criar um novo ResultSetExtractorpara a tarefa.

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

Criar um ResultSetExtractoragora se torna 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 isso ajude a mostrar que agora você pode combinar facilmente partes de uma maneira poderosa para tornar seu domínio mais simples.

ATUALIZAÇÃO 2 : Combine com um Opcional para valores opcionais em vez de nulo.

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

Que agora, quando usado, poderia ter o seguinte:

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);
}

21

Essa não é uma boa solução porque você está contando com exceções para o fluxo de controle. Na sua solução é normal obter exceções, é normal tê-las no log.

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);
    }
}

minha solução pode não ser a mais elegante, mas pelo menos a minha funciona. Você dá um exemplo de queryForObjectList que nem mesmo é uma opção com Jdbctemplate.
Byron

1
A única desvantagem aqui é que se o tipo de retorno fosse um tipo complexo, você estaria construindo vários objetos e instanciando uma lista, também ResultSet.next()seria chamado desnecessariamente. Usar a ResultSetExtractoré uma ferramenta muito mais eficiente neste caso.
Brett Ryan

e se não ter valor for uma opção, mas não ter mais de um? Eu tenho esse padrão com frequência e gostaria de ter um queryForOptionalObject no Spring para essa finalidade.
Guillaume

7

Ok, eu descobri. Acabei de envolvê-lo em um try catch e enviar de volta null.

    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;
    }

1
Não vejo por que isso é tão ruim e por que você recebeu tantos votos negativos por isso, aparte por ser um fundamentalista no princípio de 'nenhum fluxo de programa dentro da exceção'. Eu teria apenas substituído o rastreamento da pilha de impressão por um comentário explicando o caso e nada mais.
Guillaume

7

Na verdade, você pode brincar JdbcTemplatee personalizar seu próprio método como preferir. Minha sugestão é fazer algo assim:

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 o original jdbc.queryForObject, mas sem throw new EmptyResultDataAccessExceptionquando size == 0.


@Abdull UserMapper implements RowMapper<String>.
Brett Ryan de

Eu acho que é a melhor resposta aqui, pois dá a sintaxe mais curta
Stan Sokolov

DataAccessUtils.singleResult(...)é o que eu estava procurando. Thx
Drakes

7

Como retornar um valor nulo quando não há dados é algo que eu quero fazer com frequência ao usar queryForObject, achei útil estender JdbcTemplate e adicionar um método queryForNullableObject semelhante ao abaixo.

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));
    }

}

Agora você pode usar isso em seu código da mesma maneira que usou queryForObject

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

Gostaria de saber se mais alguém acha que isso é uma boa ideia.


1
É, e deveria ser na primavera
Guillaume

4

Usando Java 8 ou superior, você pode usar um Optional e Java Streams.

Então você pode simplesmente usar o JdbcTemplate.queryForList()método, criar um Stream e usar o Stream.findFirst()que retornará o primeiro valor do Stream ou um vazio 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 melhorar o desempenho da consulta, você pode anexar LIMIT 1à sua consulta, de forma que não mais de 1 item seja transferido do banco de dados.


1
Legal e Limpo. Sem ifs ou lambdas adicionais. Eu gosto disso.
BeshEater

2

Você pode usar uma função de grupo para que sua consulta sempre retorne um resultado. ie

MIN(ID_NMB_SRZ)

1

No Postgres, você pode fazer com que quase qualquer consulta de valor único retorne um valor ou nulo envolvendo-o:

SELECT (SELECT <query>) AS value

e, portanto, evita a complexidade do chamador.


1

Já que getJdbcTemplate (). QueryForMap espera tamanho mínimo de um, mas quando retorna null mostra EmptyResultDataAccesso corrige dis quando pode usar a lógica abaixo

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;
}

0

Eu lidei com isso antes e havia postado nos fóruns da primavera.

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

O conselho que recebemos foi usar um tipo de SQlQuery. Aqui está um exemplo do que fizemos ao tentar obter um valor de um banco de dados que pode não estar lá.

@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);
        }

No DAO então apenas ligamos ...

Long id = findID.findObject(id);

Não está claro no desempenho, mas funciona e é legal.


0

Para Byron, você pode tentar isso ..

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();
        }

0

fazer

    jdbcTemplate.queryForList(sql, String.class)

trabalho, certifique-se de que seu jdbcTemplate é do tipo

    org.springframework.jdbc.core.JdbcTemplate

0

Podemos usar query em vez de queryForObject, a principal diferença entre query e queryForObject é que a lista de retorno da consulta do Object (com base no tipo de retorno do Row mapper) e essa lista pode estar vazia se nenhum dado for recebido do banco de dados, enquanto queryForObject sempre espera que apenas um único objeto seja buscado do banco de dados nem nulo nem linhas múltiplas e, caso o resultado seja vazio, queryForObject lança EmptyResultDataAccessException. Eu escrevi um código usando consulta que superará o problema de EmptyResultDataAccessException em 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);
      }
  }

0

IMHO retornar um nullé uma solução ruim porque agora você tem o problema de enviar e interpretar no (provável) cliente front-end. Eu tive o mesmo erro e resolvi simplesmente retornando a List<FooObject>. eu useiJDBCTemplate.query() .

No front end (cliente Web Angular), eu simplesmente examino a lista e, se ela estiver vazia (de comprimento zero), a considero como nenhum registro encontrado.


-1

Acabei de pegar 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;
    }
}

então você pode verificar:

if(m == null){
    // insert operation.
}else{
    // update operation.
}

Podemos usar consulta em vez de
queryForObject

1
Normalmente considerado uma má prática abusar de exceções como esta. As exceções não são para fluxo lógico de programa previsível, mas para situações excepcionais.
Chris Baker
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.