Запит Jdbctemplate для рядка: EmptyResultDataAccessException: Неправильний розмір результату: очікуваний 1, фактичний 0


105

Я використовую Jdbctemplate для отримання одного значення String з db. Ось мій метод.

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

У моєму сценарії цілком можливо НЕ отримати попадання в мій запит, тому моє питання полягає в тому, як мені обійти таке повідомлення про помилку.

EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0

Мені здалося б, що я повинен просто повернути нуль, а не кидати виняток. Як я можу це виправити? Заздалегідь спасибі.

Відповіді:


179

У JdbcTemplate, queryForInt, queryForLong, queryForObjectвсе такі методи очікує , що виконані запит буде повертати одну і тільки один рядок. Якщо у вас немає рядків або більше одного ряду, це призведе до IncorrectResultSizeDataAccessException. Тепер правильним способом є не виловлювати цей виняток EmptyResultDataAccessException, але переконайтесь, що запит, який ви використовуєте, повинен повертати лише один рядок. Якщо це взагалі неможливо, тоді скористайтеся queryметодом.

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

Як було зазначено нижче, єдиний недолік тут полягає в тому, що якби тип повернення був складним типом, ви створювали б декілька об'єктів та створювали список, також ResultSet.next()були б викликані без потреби. Використання a ResultSetExtractorє набагато ефективнішим інструментом у цьому випадку.
Бретт Райан

3
В анонімному визначенні класу відсутні дужки - новий RowMapper ()
Janis Koluzs

Я з Бреттом на цьому. ResultSetExtractor чистіший :)
laher

2
Привіт @ Ракеш, чому не тільки return nullв catch(EmptyResultDataAccessException exception){ return null; }?
Вішал Занзрукіа

1
Гей! Чи можу я просто запитати, чому "Тепер правильний спосіб - це не зловити цей виняток", враховуючи, чи ви використовуєте queryForObject? Що було б неправильно з вилученням виключення у випадку запитуForObject? Дякую :)
Майкл Стоукс

48

Ви також можете використовувати ResultSetExtractorзамість RowMapper. Обидва так само легкі, як один одному, різниця лише в тому, що ви телефонуєте 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;
        }
    });
}

ResultSetExtractorМає додаткову перевагу , що ви можете обробляти всі випадки , коли Є більш ніж один рядок чи ні рядків , що повертаються.

ОНОВЛЕННЯ : Кілька років і у мене є декілька хитрощів. JdbcTemplateчудово працює з лямбдами java 8, для яких розроблені наступні приклади, але ви можете досить легко використовувати статичний клас для досягнення цього.

Хоча питання стосується простих типів, ці приклади служать керівництвом для загального випадку вилучення об’єктів домену.

По-перше. Припустимо, у вас є об’єкт облікового запису з двома властивостями для простоти Account(Long id, String name). Ви, ймовірно, хочете мати RowMapperдля цього об’єкта домену.

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

Тепер ви можете використовувати цей картограф безпосередньо в методі для відображення Accountоб’єктів домену за запитом ( jtце JdbcTemplateекземпляр).

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

Чудово, але зараз ми хочемо нашої оригінальної проблеми, і ми використовуємо моє оригінальне рішення, яке повторно використовує RowMapperдля виконання карти для нас.

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

Чудово, але це шаблон, який ви можете і хочете повторити. Таким чином, ви можете створити загальний заводський метод для створення нового ResultSetExtractorдля завдання.

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

Створення ResultSetExtractorтепер стає банальним.

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

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

Я сподіваюся, що це допоможе показати, що тепер ви можете досить легко комбінувати деталі потужним чином, щоб зробити свій домен простішим.

ОНОВЛЕННЯ 2 : комбінуйте з необов'язковими для необов'язкових значень замість null.

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

Що зараз при використанні може мати наступне:

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

Це не гарне рішення, оскільки ви покладаєтесь на винятки для контролю потоку. У вашому рішенні нормально отримувати винятки, нормально мати їх у журналі.

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

моє рішення може бути не найелегантнішим, але принаймні моїми роботами. Ви наводите приклад queryForObjectList, який навіть не є опцією для Jdbctemplate.
Байрон

1
Єдиним недоліком тут є те, що якби тип повернення був складним типом, ви створювали б декілька об'єктів та створювали список, також ResultSet.next()були б викликані без потреби. Використання a ResultSetExtractorє набагато ефективнішим інструментом у цьому випадку.
Бретт Райан

а що, якщо немає значення - це варіант, але не має більше одного? У мене є такий шаблон часто, і я хотів би, щоб навесні був запитForOtionalObject навесні.
Гійом

7

Насправді ви можете грати JdbcTemplateі налаштовувати свій власний метод, як вам зручніше. Я пропоную зробити щось подібне:

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

Він працює як оригінал jdbc.queryForObject, але без того, throw new EmptyResultDataAccessExceptionколи size == 0.


@Abdull UserMapper implements RowMapper<String>.
Бретт Райан

Я думаю, що це найкраща відповідь тут, оскільки він дає найкоротший синтаксис
Стен Соколов

DataAccessUtils.singleResult(...)це те, що я шукав. Thx
Drakes

7

Оскільки я повертаю нуль, коли немає даних, це те, що я хочу робити часто під час використання queryForObject, я вважаю корисним розширити JdbcTemplate і додати метод queryForNullableObject, подібний до наведеного нижче.

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

}

Тепер ви можете використовувати це у своєму коді так само, як і queryForObject

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

Мені було б цікаво дізнатися, чи хтось вважає, що це гарна ідея?


1
Так і має бути весною
Гійом

6

Гаразд, я зрозумів це. Я просто загорнув його в спробу лову і повернути 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
Я не бачу, чому це так погано, і чому ви отримали стільки голосів за це, наприклад, будучи фундаменталістом за принципом "немає програмного потоку в рамках виключення". Я б просто замінив слід printstack коментарем, що пояснює випадок, і більше нічого не робити.
Гійом

4

Використовуючи Java 8 або вище, ви можете використовувати Optionalі потоки Java.

Таким чином, ви можете просто скористатися JdbcTemplate.queryForList()методом, створити Потік і використовувати, Stream.findFirst()який поверне перше значення Потоку або порожнє 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();
}

Для підвищення продуктивності запиту ви можете додати LIMIT 1до свого запиту, тому з бази даних передається не більше 1 елемента.


1
Приємно і чисто. Ніяких додаткових інфа чи лямбда. Мені це подобається.
BeshEater

2

Ви можете використовувати групову функцію, щоб ваш запит завжди повертав результат. тобто

MIN(ID_NMB_SRZ)

1

У Postgres ви можете змусити практично будь-який запит на одне значення повернути значення або null, обернувши його:

SELECT (SELECT <query>) AS value

а отже, уникнути складності в абонента.


1

Оскільки getJdbcTemplate (). QueryForMap очікує мінімального розміру одиниці, але коли він повернеться null, він показує EmptyResultDataAccesso fix dis, коли можна використовувати нижче логіки

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

Я займався цим раніше і публікував на весняних форумах.

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

Ми отримали пораду щодо використання типу SQlQuery. Ось приклад того, що ми зробили, намагаючись отримати значення з БД, яке там може бути.

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

У DAO тоді ми просто телефонуємо ...

Long id = findID.findObject(id);

Незрозуміло щодо продуктивності, але воно працює і акуратно.


0

Для Байрона ви можете спробувати це ..

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

робити

    jdbcTemplate.queryForList(sql, String.class)

працюйте, переконайтеся, що ваш jdbcTemplate має тип

    org.springframework.jdbc.core.JdbcTemplate

0

Ми можемо використовувати запит замість queryForObject, основна відмінність між запитом і queryForObject полягає в тому, що список повернення запиту Об'єкта (заснований на типі повернення картографічного рядка) і цей список може бути порожнім, якщо дані не отримані з бази даних, тоді як queryForObject завжди очікує, що буде лише один об'єкт витягнуто з db ні null, ні кілька рядків, і якщо результат порожній, тоді queryForObject кидає EmptyResultDataAccessException, я написав один код, використовуючи запит, який подолає проблему EmptyResultDataAccessException у разі нульового результату.

----------


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 null- це погане рішення, тому що зараз у вас є проблема надсилання та інтерпретації цього клієнта (ймовірно) переднього клієнта. У мене була така ж помилка, і я вирішив її, просто повернувши а List<FooObject>. Я звик JDBCTemplate.query().

На передньому кінці (Angular web client) я просто вивчаю список і якщо він порожній (нульової довжини), вважаю його таким, що не знайдено записів.


-1

Я просто спіймаю це "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;
    }
}

то ви можете перевірити:

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

Ми можемо використовувати запит замість queryForObject
ABHAY JOHRI

1
Зазвичай вважається поганою практикою зловживання винятками, як це. Винятки не є передбачуваною логікою потоку програми, вони є для виключних ситуацій.
Кріс Бейкер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.