Jdbi3官方教程(七) 结果Result

Jdbi3官方教程(一) 简介
Jdbi3官方教程(二) 入门
Jdbi3官方教程(三) Jdbi和Handle
Jdbi3官方教程(四) 参数绑定
Jdbi3官方教程(五) 查询Query
Jdbi3官方教程(六) 映射器Mapper

3.7 结果Result

执行数据库查询后,您需要解析结果。JDBC提供了ResultSet类,它可以简单地映射到Java原生类型和内置类,但API通常很难使用。Jdbi提供可配置的映射,包括为行和列注册自定义映射器的功能。

RowMapper将一个行结果集转换成结果对象。

ColumnMapper将单个列的值转换为Java对象。它可以被用来作为一个只有一列存在RowMapper,或者它可以被用来构建更复杂的RowMapper类型。

根据查询的声明结果类型选择映射器。

jdbi迭代ResultSet中的行,并在容器(如ListStreamOptionalIterator)向您显示映射结果。

public static class User {
    final int id;
    final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

@Before
public void setUp() throws Exception {
    handle.execute("CREATE TABLE user (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR)");
    for (String name : Arrays.asList("Alice", "Bob", "Charlie", "Data")) {
        handle.execute("INSERT INTO user(name) VALUES (?)", name);
    }
}

@Test
public void findBob() {
    User u = findUserById(2).orElseThrow(() -> new AssertionError("No user found"));
    assertThat(u.id).isEqualTo(2);
    assertThat(u.name).isEqualTo("Bob");
}

public Optional<User> findUserById(long id) {
    RowMapper<User> userMapper =
            (rs, ctx) -> new User(rs.getInt("id"), rs.getString("name"));
    return handle.createQuery("SELECT * FROM user WHERE id=:id")
        .bind("id", id)
        .map(userMapper)
        .findFirst();
}

3.7.1 ResultBearing

ResultBearing接口表示数据库操作的结果,还没有被映射到任何特定结果类型的结果。

3.7.2 ResultIterable

ResultIterable表示已映射到特定类型的结果集,例如 ResultIterable<User>

查找单个结果

ResultIterable#findOnly返回结果集中的唯一行。如果遇到零行或多行,则会抛出IllegalStateException

#findFirst返回带有第一行的Optional <T>(如果有)。

集成允许您使用RowMapper将ResultSet调整为新的Java 8 Streams框架。只要您的数据库支持流式传输结果(例如,只要您在事务中并且设置了获取大小,PostgreSQL就会执行此操作),流将根据需要从数据库中懒惰地获取行。

#stream返回Stream <T>。然后,您应该处理流并生成结果。必须关闭此流以释放所持有的任何数据库资源,因此我们建议使用withStreamwithStream或者try-with-resources块来确保不泄漏任何资源。

handle.createQuery("SELECT id, name FROM user ORDER BY id ASC")
      .map(new UserMapper())
      .useStream(stream -> {
          Optional<String> first = stream
              .filter(u -> u.id > 2)
              .map(u -> u.name)
              .findFirst();
          assertThat(first).contains("Charlie");
      });

#withStream#useStream处理为您关闭流。您提供了一个产生结果的StreamCallback或一个不产生结果的StreamConsumer

列表

#list发出List <T>。这必然会将所有结果缓存在内存中。

List<User> users =
    handle.createQuery("SELECT id, name FROM user")
        .map(new UserMapper())
        .list();
集合

#collect需要收集<T,?,R>构建一个结果集合 R <T>java.util.stream.Collectors类有一些有趣的Collector

您也可以编写自己的自定义Collector。例如,要将找到的行放到Map中

h.execute("insert into something (id, name) values (1, 'Alice'), (2, 'Bob'), (3, 'Chuckles')");
Map<Integer, Something> users = h.createQuery("select id, name from something")
    .mapTo(Something.class)
    .collect(Collector.of(HashMap::new, (accum, item) -> {
        accum.put(item.getId(), item);   // Each entry is added into an accumulator map
    }, (l, r) -> {
        l.putAll(r);                     // While jdbi does not process rows in parallel,
        return l;                        // the Collector contract encourages writing combiners.
    }, Characteristics.IDENTITY_FINISH));
Reduce

#reduce提供了简化的Stream#reduce。给定一个起始值和一个BiFunction <U,T,U>它将重复组合* U * s,直到只剩下一个,然后返回。

ResultSetScanner

ResultSetScanner接口接受一个懒加载的结果集 ,并返回Jdbi执行语句的结果。

上述大多数操作都是根据ResultSetScanner实现的。扫描程序拥有ResultSet的所有权,可以提前或搜索它。

返回值是语句执行的最终结果。

大多数用户应该更喜欢使用上面描述的更高级别的结果收集器,但是必须做点工作。

3.7.3 连接

将多个表连接在一起是一项非常常见的数据库任务。它也是关系模型和Java的对象模型之间的不匹配的开始。

在这里,我们提出了一些从更复杂的行中检索结果的策略。

以联系人列表应用为例。联系人列表包含任意数量的联系人。联系人有姓名和任意数量的电话号码。电话号码有类型(例如家庭,工作)和电话号码:

class Contact {
  Long id;
  String name;
  List<Phone> phones = new ArrayList<>();

  void addPhone(Phone phone) {
    phones.add(phone);
  }
}

class Phone {
  Long id;
  String type;
  String phone;
}

为简洁起见,我们省略了getter,setter和访问修饰符。

由于我们将重用相同的查询,我们现在将它们定义为常量:

static final String SELECT_ALL = "select contacts.id c_id, name c_name, "
    + "phones.id p_id, type p_type, phones.phone p_phone "
    + "from contacts left join phones on contacts.id = phones.contact_id "
    + "order by c_name, p_type ";

static final String SELECT_ONE = SELECT_ALL + "where phones.id = :id";

请注意,我们已经给别名(例如c_idp_id)来区分(同一名称的列id)从不同的表。

Jdbi提供了一些用于处理联接数据的不同API。

ResultBearing.reduceRows()

ResultBearing.reduceRows(U,BiFunction) 方法接受一个累加器初始值和lambda函数。对于结果集中的每一行,Jdbi使用当前累加器值调用lambda,并在结果集的当前行上调用 RowView。为每行返回的值将成为传入下一行的输入累加器。处理完最后一行后, reducedRows()返回lambda返回的最后一个值。

List<Contact> contacts = handle.createQuery(SELECT_ALL)
    .registerRowMapper(BeanMapper.factory(Contact.class, "c"))
    .registerRowMapper(BeanMapper.factory(Phone.class, "p")) 
    .reduceRows(new LinkedHashMap<Long, Contact>(), 
                (map, rowView) -> {
      Contact contact = map.computeIfAbsent( 
          rowView.getColumn("c_id", Long.class),
          id -> rowView.getRow(Contact.class));

      if (rowView.getColumn("p_id", Long.class) != null) { 
        contact.addPhone(rowView.getRow(Phone.class));
      }

      return map; 
    })
    .values() 
    .stream()
    .collect(toList()); 
  • 为Contact和Phone注册行映射器。注意使用的"c"和"p" 参数 - 这些是列名前缀。通过前缀注册映射器,该Contact映射器将只映射c_id和c_name 列,而Phone映射器将只映射p_id,p_type和 p_phone。
  • 使用空的LinkedHashMap 作为累加器初始值,按联系人ID映射。选择多个主记录时,LinkedHashMap是一个很好的累加器,因为它具有快速存储和查找,同时保留了插入顺序(这有助于遵守 ORDER BY条款)。如果顺序不重要,那HashMap也就足够了。
  • 如果我们已经拥有Contact,从累加器加载; 否则,通过RowView。初始化它。
  • 如果p_idcolumn不为null,从当前行加载电话号码并将其添加到当前联系人。
  • todo
  • 返回输入的map(现在运行额外的联系人/电话)作为下一行的累加器。
  • 此时,所有行都已读入内存,我们不需要联系人ID键。所以我们打电话Map.values()来得到一个Collection<Contact>。
  • 将联系人收集到一个List<Contact>。

或者, ResultBearing.reduceRows(RowReducer) 变体接受RowReducer并返回简化元素流。

对于简单的master-detail连接, ResultBearing.reduceRows - BiConsumer 方法可以轻松地将这些连接reduce为主元素流。

调整上面的例子:

List<Contact> contacts = handle.createQuery(SELECT_ALL)
    .registerRowMapper(BeanMapper.factory(Contact.class, "c"))
    .registerRowMapper(BeanMapper.factory(Phone.class, "p"))
    .reduceRows((Map<Long, Contact> map, RowView rowView) -> { 
      Contact contact = map.computeIfAbsent(
          rowView.getColumn("c_id", Long.class),
          id -> rowView.getRow(Contact.class));

      if (rowView.getColumn("p_id", Long.class) != null) {
        contact.addPhone(rowView.getRow(Phone.class));
      }
      
    })
    .collect(toList()); 
  • lambda接收一个存储结果对象的map,以及一个 RowView。映射是LinkedHashMap,因此结果流将按照插入的顺序生成结果对象。
  • 不需要任何return语句。map每一行都重复使用相同的内容。
  • reduceRows()调用产生一个Stream<Contact>(即from map.values().stream()。在这个例子中,我们将元素收集到一个列表中,但我们可以在Stream这里调用任何方法。

你可能想知道getRow()getColumn()调用rowView。当您调用时rowView.getRow(SomeType.class)RowView查找已注册的行映射器SomeType,并使用它将当前行映射到 SomeType对象。

同样,当您调用时rowView.getColumn("my_value", MyValueType.class)RowView查找已注册的列映射器MyValueType,并使用它将my_value当前行的列映射到MyValueType对象。

现在让我们做同样的事情,但对于一个联系人:

Optional<Contact> contact = handle.createQuery(SELECT_ONE)
    .bind("id", contactId)
    .registerRowMapper(BeanMapper.factory(Contact.class, "c"))
    .registerRowMapper(BeanMapper.factory(Phone.class, "p"))
    .reduceRows(LinkedHashMapRowReducer.<Long, Contact> of((map, rowView) -> {
      Contact contact = map.orElseGet(() -> rowView.getRow(Contact.class));

      if (rowView.getColumn("p_id", Long.class) != null) {
        contact.addPhone(rowView.getRow(Phone.class));
      }
    })
    .findFirst();
ResultBearing.reduceResultSet()

ResultBearing.reduceResultSet() 是一个类似的低级API reduceRows(),除了它提供对JDBC的直接访问,ResultSet而不是RowView每行的访问。

reduceRows()冗长相比,这种方法可以提供卓越的性能:

List<Contact> contacts = handle.createQuery(SELECT_ALL)
    .reduceResultSet(new LinkedHashMap<Long, Contact>(),
                     (acc, resultSet, ctx) -> {
      long contactId = resultSet.getLong("c_id");
      Contact contact;
      if (acc.containsKey(contactId)) {
        contact = acc.get(contactId);
      } else {
        contact = new Contact();
        contact.setId(contactId);
        contact.setName(resultSet.getString("c_name");
      }

      long phoneId = resultSet.getLong("p_id");
      if (!resultSet.wasNull()) {
        Phone phone = new Phone();
        phone.setId(phoneId);
        phone.setType(resultSet.getString("p_type");
        phone.setPhone(resultSet.getString("p_phone");
        contact.addPhone(phone);
      }

      return acc;
    })
    .values()
    .stream()
    .collect(toList());
JoinRowMapper

JoinRowMapper从一行中提取一组类型。它使用映射注册表来确定如何映射每个给定类型,并为您提供一个包含所有结果值的JoinRow。

让我们考虑两个简单的类型,User和Article,以及一个名为Author的连接表。Guava提供了一个Multimap类,它非常便于表示像这样的连接表。假设我们已经注册了映射器:

h.registerRowMapper(ConstructorMapper.factory(User.class));
h.registerRowMapper(ConstructorMapper.factory(Article.class));

然后,我们可以使用数据库中的映射轻松填充Multimap:

Multimap<User, Article> joined = HashMultimap.create();
h.createQuery("SELECT * FROM user NATURAL JOIN author NATURAL JOIN article")
    .map(JoinRowMapper.forTypes(User.class, Article.class))
    .forEach(jr -> joined.put(jr.get(User.class), jr.get(Article.class)));

虽然这种方法易于读写,但对于某些数据模式来说效率低下。在决定是使用高级映射还是使用手写映射器进行更直接的低级访问时,请考虑性能要求。

您也可以将它与SqlObject一起使用:

public interface UserArticleDao {
    @RegisterJoinRowMapper({User.class, Article.class})
    @SqlQuery("SELECT * FROM user NATURAL JOIN author NATURAL JOIN article")
    Stream<JoinRow> getAuthorship();
}
Multimap<User, Article> joined = HashMultimap.create();

handle.attach(UserArticleDao.class)
        .getAuthorship()
        .forEach(jr -> joined.put(jr.get(User.class), jr.get(Article.class)));

assertThat(joined).isEqualTo(JoinRowMapperTest.getExpected());
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容