3.7 结果Result
执行数据库查询后,您需要解析结果。JDBC提供了ResultSet类,它可以简单地映射到Java原生类型和内置类,但API通常很难使用。Jdbi提供可配置的映射,包括为行和列注册自定义映射器的功能。
RowMapper将一个行结果集转换成结果对象。
ColumnMapper将单个列的值转换为Java对象。它可以被用来作为一个只有一列存在RowMapper,或者它可以被用来构建更复杂的RowMapper类型。
根据查询的声明结果类型选择映射器。
jdbi迭代ResultSet中的行,并在容器(如List,Stream,Optional或Iterator)向您显示映射结果。
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>。然后,您应该处理流并生成结果。必须关闭此流以释放所持有的任何数据库资源,因此我们建议使用withStream,withStream或者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_id
,p_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>
(即frommap.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());
网友评论