JDBC ResultSet и RowSet

Базы данных всё таки о данных, а не о запросах. В JDBC данные, которые возвращают запросы, представлены в виде объектов ResultSet.

ResultSet, в свою очередь, жёстко связан со Statement, который его породил и существует только до момента закрытия этого самого Statement  или даже раньше, до выполнения нового запроса в Statement.

Для доступа к данным интерфейс ResultSet реализует смесь шаблонов Итератор и Курсор: внутри ResultSet есть указатель, который указывает на какую-либо строку (или даже не на строку, а в никуда) в данных. Этот указатель можно передвигать программно и запрашивать данные из столбцов текущей строки. По умолчанию курсор ResultSet находится перед первой строкой набора данных.

Существует целых восемь методов, перемещающих курсор по ResultSet:

  • next() — перемещает курсор на одну строку вперёд. Возвращает true, если перемещение удалось и false, если курсор уже находится за последней строкой.
  • previous() — очевидно, антоним next(). Перемещает курсорс на одну строку назад и тоже возвращает true, если перемещение удалось и false, если курсор находится перед первой строкой.
  • first() и last() — перемещают курсор соответственно на первую и последнюю строку набора данных. В случае, если набор данных пуст, возвращают false. В случае успешного перемещения возвращают true.
  • beforeFirst() и afterLast() — перемещают курсор на позицию перед первой строкой или после последней строки.
  • relative() — перемещает курсор на указанное число строк от текущей позиции.
  • absolute() — перемещает курсор на указанное число строк от первой позиции.

Стоит отметить, что не все эти методы всегда работают. ResultSet (а точнее конкретная его реализация драйвером JDBC) может не поддерживать перемещение кроме как вперёд. Такой ResultSet называется TYPE_FORWARD_ONLY. В случае, если перемещение возможно, открытый ResultSet может следить за изменениями в базе данных, произошедшими после его открытия или не следить. В первом случае это будет TYPE_SCROLL_SENSITIVE ResultSet, во втором TYPE_SCROLL_INSENSITIVE.

Чтение из ResultSet

Читать из ResultSet немного не интуитивно, но сравнительно просто: перемещаем курсор в нужные строки и запрашиваем содержимое столбцов.

protected static void readResultSet(Connection db) throws SQLException {
  System.out.println("Dumping ORDER_ITEMS table:");
  try (Statement results = db.createStatement()) {
    ResultSet rs =
        results.executeQuery("SELECT * FROM ORDER_ITEMS");
    while (rs.next()) {
      System.out.println(
        String.format(
          "client=%d, order=%d, item=%d",
          rs.getInt("CLIENT_ID"),
          rs.getInt("ORDER_ID"),
          rs.getInt(3)));
     }
  }
}
Dumping ORDER_ITEMS table:
client=1, order=1, item=1
client=1, order=1, item=2
client=1, order=1, item=3
client=1, order=1, item=4
client=1, order=1, item=5
client=2, order=2, item=1


[skip]
client=3, order=3, item=4
client=3, order=3, item=5

Методы getType() возвращают объект нужного типа, с учётом типа в базе данных. К столбцам можно обращаться как по имени, причем без учёта регистра, так и по номеру, причём отсчёт столбцов начинается с единицы.

Запись в ResultSet

Да да, именно запись в ResultSet. JDBC позволяет не только читать данные из ResultSet, но и записывать их обратно и эти изменения будут автоматически переданы в базу. Конечно, такое поведение может быть несколько непривычно для тех, кто работает с SQL базами данных, но JDBC рассчитан не только на SQL базы, но и например, на какой-нибудь там FoxPro, в котором именно так данные и обновляют (или обновляли, лет 20 назад).

Обновление данных производится не сложнее, чем чтение: выбираем строку, пишем в столбцы и сохраняем.

protected static void updateResultSet(Connection db) throws SQLException {
        try (Statement updatableResult = db.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                ResultSet.CONCUR_UPDATABLE)) {
            ResultSet rs =
                    updatableResult.executeQuery("SELECT * FROM ORDER_ITEMS");
            rs.absolute(5);
            rs.updateInt("CLIENT_ID", 2);
            rs.updateRow();


            rs.moveToInsertRow();
            rs.updateInt("CLIENT_ID", 1);
            rs.updateInt("ORDER_ID", 1);
            rs.updateInt("ITEM_ID", 10);
            rs.insertRow();
        }
    }
Dumping ORDER_ITEMS table:
client=1, order=1, item=1
client=1, order=1, item=2
client=1, order=1, item=3
client=1, order=1, item=4
client=2, order=1, item=5
client=2, order=2, item=1
[skip]
client=3, order=3, item=5
client=1, order=1, item=10

Вторая часть кода из примера показывает, что строки можно не только обновлять но и добавлять! Добавляются строки почти так же, как и обновляются: перемещаемся в специальное место(ага, девятый метод позиционирования в ResultSet), обновляем значения столбцов, вставляем строку.

Опять таки, не всякий ResultSet может обновлять данные, а только созданный с CONCUR_UPDATABLE и только если драйвер JDBC этот режим поддерживает.

RowSet

RowSet расширяет ResultSet и делает его совместимым с концепцией JavaBean (то есть с конструктором по умолчанию, сериализуемым и т.д.). Поскольку интерфейс RowSet расширяет интерфейс ResultSet, весь вышеперечисленный функционал, разумеется, остаётся доступным и в RowSet. Главными отличиями RowSet от ResultSet является тот факт, что RowSet есть JavaBean, со свойствами и нотификациями. Кроме того, RowSet можно строить напрямую из соединения с базой, пропуская отдельно создание запроса.

Самая простая реализация RowSet — JdbcRowSet, обычно обёртка над ResultSet:

  protected static void jdbcRowSet(Connection db) throws SQLException {
    JdbcRowSet rs = new JdbcRowSetImpl(db);
    rs.setCommand("SELECT * FROM ORDER_ITEMS");
    rs.execute();


    rs.moveToInsertRow();
    rs.updateInt("CLIENT_ID", 1);
    rs.updateInt("ORDER_ID", 1);
    rs.updateInt("ITEM_ID", 11);
    rs.insertRow();


    rs.execute();
    rs.beforeFirst();
    System.out.println("Dumping ORDER_ITEMS table using RowSet:");
    while (rs.next()) {
      System.out.println(
        String.format(
           client=%d, order=%d, item=%d",
           rs.getInt("CLIENT_ID"),
           rs.getInt("ORDER_ID"),
           rs.getInt("ITEM_ID")));
    }
}

JdbcRowSet можно создать как в примере выше, а можно напрямую из ResultSet. Этот тип RowSet, так же как и ResultSet, требует наличия соединения с базой для работы с данными.

CachedRowSet

CachedRowSet, оправдывая своё название, сохраняет работоспособность и тогда, когда соединение с базой уже закрыто, сразу кэшируя в память все данные, которые вернул запрос.

protected static CachedRowSet cachedRowSet(Connection db) throws SQLException {
  try (Statement results = db.createStatement()) {
    ResultSet rs =
      results.executeQuery("SELECT * FROM ORDER_ITEMS");
    CachedRowSet cs = new CachedRowSetImpl();
    cs.populate(rs);


    return cs;
  }
}


public static void main(final String[] args) {
  CachedRowSet cs = null;
  try (Connection db = DriverManager.getConnection("jdbc:h2:mem:")) {
    cs = cachedRowSet(db);
  } catch (SQLException ex) {
    System.out.println("Database connection failure: "
      + ex.getMessage());
  } catch (IOException ex) {
    System.out.println("I/O error: "
      + ex.getMessage());
  }
  try {
    System.out.println("Dumping ORDER_ITEMS table without database connection:");
    assert cs != null;
    while(cs.next()) {
      System.out.println(
        String.format(
          "client=%d, order=%d, item=%d",
          cs.getInt("CLIENT_ID"),
          cs.getInt("ORDER_ID"),
          cs.getInt("ITEM_ID")));
    }
  } catch (SQLException ex) {
    System.out.println("Database connection failure: "
      + ex.getMessage());
  }
}

CachedRowSet можно спокойно возвращать и передавать куда угодно, не опасаясь, что его соединение с базой внезапно закроется и RowSet превратится в тыкву. Такой тип RowSet называется disconnected, как и все нижеописанные RowSet.

JoinRowSet

Делает слияние таблиц в памяти. Конечно, с точки зрения эффективности выгоднее делать join непосредственно на стороне базы, но не всякая база умеет join (вспоминаем FoxPro опять, ага). Чтобы слить таблицы, вначале необходимо получить две таблицы для слияния и потом добавить их в JoinRowSet, указав по какому полю сливать их.

protected static void joinRowSet(Connection db) throws SQLException {
  CachedRowSet orders = new CachedRowSetImpl();
  CachedRowSet clients = new CachedRowSetImpl();
  try (Statement results = db.createStatement()) {
    ResultSet rs =
      results.executeQuery("SELECT * FROM ORDER_ITEMS");
    orders.populate(rs);
  }
  try (Statement results = db.createStatement()) {
    ResultSet rs =
      results.executeQuery("SELECT * FROM CLIENTS");
    clients.populate(rs);
  }
  JoinRowSet jrs = new JoinRowSetImpl();
  jrs.addRowSet(orders, "CLIENT_ID");
  jrs.addRowSet(clients, "ID");


  System.out.println("Dumping client logins and their items:");
  while (jrs.next()) {
    System.out.println(
      String.format(
        "client=%s, order=%d",
        jrs.getString("LOGIN"),
        jrs.getInt("ORDER_ID")));
  }
}
client=test, order=1
client=test, order=1
client=example, order=3
client=example, order=3
[skip]

После для слияния указывается для каждого участника слияния отдельно. Можно использовать или название столбца (регистронезависимое) или номер (нумерация начинается с единицы). Тип столбца в обоих таблицах должен совпадать.

FilteredRowSet

RowSet который умеет сам себя фильтровать. На первый взгляд кажется бесполезной вещью (даже FoxPro умеет в условия), но на самом деле очень удобен, так как фильтры в коде могут быть гораздо гибче, чем условия выборки в базе. Да и фильтровать какой-нибудь постоянно висящий в памяти словарь становится выгоднее, чем постоянно его перезапрашивать.

Создаётся и наполняется данными FilteredRowSet как обычно, а потом ему методом setFilter() назначается объект класса, реализующего интерфейс Predicate. Причём это не тот модный функциональный интерфейс Predicate, который используется с лямбда выражениями и потоками, а собственный кондовый Predicate из JDBC. Так что никаких лямбд ????

private static class ClientFilter implements Predicate {
  @Override
  public boolean evaluate(RowSet rs) {
    try {
      return rs.getInt("CLIENT_ID") == 3;
    } catch (SQLException e) {
      return false;
    }
  }


  @Override
  public boolean evaluate(Object value, int column) throws SQLException {
    return !(column == 1 && !"3".equals(value));
  }


  @Override
  public boolean evaluate(Object value, String columnName) throws SQLException {
    return !("CLIENT_ID".equals(columnName) && !"3".equals((String) value));
  }
}


protected static void filteredRowSet(Connection db) throws SQLException {
  try (Statement results = db.createStatement()) {
    ResultSet rs =
      results.executeQuery("SELECT * FROM ORDER_ITEMS");
    FilteredRowSet fs = new FilteredRowSetImpl();
    fs.populate(rs);


    fs.setFilter(new ClientFilter());


    System.out.println("Dumping only 3rd client from ORDER_ITEMS table:");
    while (fs.next()) {
      System.out.println(
        String.format(
          "client=%d, order=%d, item=%d",
          fs.getInt("CLIENT_ID"),
          fs.getInt("ORDER_ID"),
          fs.getInt("ITEM_ID")));
      }
    }
  }
}
Dumping only 3rd client from ORDER_ITEMS table:
client=3, order=3, item=1
client=3, order=3, item=2
client=3, order=3, item=3
client=3, order=3, item=4
client=3, order=3, item=5

WebRowSet

WebRowSet умеет сам сохранять себя в XML и создавать себя из XML же. В том году, когда его изобретали, это было удивительным достижением конечно, а сейчас выглядит слегка архаично.

protected static void webRowSet(Connection db) throws SQLException, IOException {
  try (Statement results = db.createStatement()) {
    ResultSet rs =
      results.executeQuery("SELECT * FROM ORDER_ITEMS");
    WebRowSet ws = new WebRowSetImpl();
    ws.populate(rs);


    ws.writeXml(System.out);
  }
}

XML представление

<?xml version="1.0"?>
<webRowSet xmlns="http://java.sun.com/xml/ns/jdbc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/jdbc http://java.sun.com/xml/ns/jdbc/webrowset.xsd">
  <properties>
    <command><null/></command>
    <concurrency>1008</concurrency>
    <datasource><null/></datasource>
    <escape-processing>true</escape-processing>
    <fetch-direction>1000</fetch-direction>
    <fetch-size>0</fetch-size>
    <isolation-level>2</isolation-level>
    <key-columns>
    </key-columns>
    <map>
    </map>
    <max-field-size>0</max-field-size>
    <max-rows>0</max-rows>
    <query-timeout>0</query-timeout>
    <read-only>true</read-only>
    <rowset-type>ResultSet.TYPE_SCROLL_INSENSITIVE</rowset-type>
    <show-deleted>false</show-deleted>
    <table-name><null/></table-name>
    <url><null/></url>
    <sync-provider>
      <sync-provider-name>com.sun.rowset.providers.RIOptimisticProvider</sync-provider-name>
      <sync-provider-vendor>Oracle Corporation</sync-provider-vendor>
      <sync-provider-version>1.0</sync-provider-version>
      <sync-provider-grade>2</sync-provider-grade>
      <data-source-lock>1</data-source-lock>
    </sync-provider>
  </properties>
  <metadata>
    <column-count>4</column-count>
    <column-definition>
      <column-index>1</column-index>
      <auto-increment>true</auto-increment>
      <case-sensitive>true</case-sensitive>
      <currency>false</currency>
      <nullable>0</nullable>
      <signed>true</signed>
      <searchable>true</searchable>
      <column-display-size>20</column-display-size>
      <column-label>ID</column-label>
      <column-name>ID</column-name>
      <schema-name>PUBLIC</schema-name>
      <column-precision>19</column-precision>
      <column-scale>0</column-scale>
      <table-name>ORDER_ITEMS</table-name>
      <catalog-name>UNNAMED</catalog-name>
      <column-type>-5</column-type>
      <column-type-name>BIGINT</column-type-name>
    </column-definition>
    <column-definition>
      <column-index>2</column-index>
      <auto-increment>false</auto-increment>
      <case-sensitive>true</case-sensitive>
      <currency>false</currency>
      <nullable>0</nullable>
      <signed>true</signed>
      <searchable>true</searchable>
      <column-display-size>20</column-display-size>
      <column-label>CLIENT_ID</column-label>
      <column-name>CLIENT_ID</column-name>
      <schema-name>PUBLIC</schema-name>
      <column-precision>19</column-precision>
      <column-scale>0</column-scale>
      <table-name>ORDER_ITEMS</table-name>
      <catalog-name>UNNAMED</catalog-name>
      <column-type>-5</column-type>
      <column-type-name>BIGINT</column-type-name>
    </column-definition>
    <column-definition>
      <column-index>3</column-index>
      <auto-increment>false</auto-increment>
      <case-sensitive>true</case-sensitive>
      <currency>false</currency>
      <nullable>0</nullable>
      <signed>true</signed>
      <searchable>true</searchable>
      <column-display-size>11</column-display-size>
      <column-label>ORDER_ID</column-label>
      <column-name>ORDER_ID</column-name>
      <schema-name>PUBLIC</schema-name>
      <column-precision>10</column-precision>
      <column-scale>0</column-scale>
      <table-name>ORDER_ITEMS</table-name>
      <catalog-name>UNNAMED</catalog-name>
      <column-type>4</column-type>
      <column-type-name>INTEGER</column-type-name>
    </column-definition>
    <column-definition>
      <column-index>4</column-index>
      <auto-increment>false</auto-increment>
      <case-sensitive>true</case-sensitive>
      <currency>false</currency>
      <nullable>0</nullable>
      <signed>true</signed>
      <searchable>true</searchable>
      <column-display-size>11</column-display-size>
      <column-label>ITEM_ID</column-label>
      <column-name>ITEM_ID</column-name>
      <schema-name>PUBLIC</schema-name>
      <column-precision>10</column-precision>
      <column-scale>0</column-scale>
      <table-name>ORDER_ITEMS</table-name>
      <catalog-name>UNNAMED</catalog-name>
      <column-type>4</column-type>
      <column-type-name>INTEGER</column-type-name>
    </column-definition>
  </metadata>
  <data>
    <currentRow>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>2</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>2</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>3</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>3</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>4</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>4</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>5</columnValue>
      <columnValue>2</columnValue>
      <columnValue>1</columnValue>
      <columnValue>5</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>6</columnValue>
      <columnValue>2</columnValue>
      <columnValue>2</columnValue>
      <columnValue>1</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>7</columnValue>
      <columnValue>2</columnValue>
      <columnValue>2</columnValue>
      <columnValue>2</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>8</columnValue>
      <columnValue>2</columnValue>
      <columnValue>2</columnValue>
      <columnValue>3</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>9</columnValue>
      <columnValue>2</columnValue>
      <columnValue>2</columnValue>
      <columnValue>4</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>10</columnValue>
      <columnValue>2</columnValue>
      <columnValue>2</columnValue>
      <columnValue>5</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>11</columnValue>
      <columnValue>3</columnValue>
      <columnValue>3</columnValue>
      <columnValue>1</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>12</columnValue>
      <columnValue>3</columnValue>
      <columnValue>3</columnValue>
      <columnValue>2</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>13</columnValue>
      <columnValue>3</columnValue>
      <columnValue>3</columnValue>
      <columnValue>3</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>14</columnValue>
      <columnValue>3</columnValue>
      <columnValue>3</columnValue>
      <columnValue>4</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>15</columnValue>
      <columnValue>3</columnValue>
      <columnValue>3</columnValue>
      <columnValue>5</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>16</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>10</columnValue>
    </currentRow>
    <currentRow>
      <columnValue>17</columnValue>
      <columnValue>1</columnValue>
      <columnValue>1</columnValue>
      <columnValue>11</columnValue>
    </currentRow>
  </data>
</webRowSet>

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *