Spring JDBC RowMapper 用于急切获取
Posted
技术标签:
【中文标题】Spring JDBC RowMapper 用于急切获取【英文标题】:Spring JDBC RowMapper usage for eager fetches 【发布时间】:2012-07-12 09:40:30 【问题描述】:问题是关于 RowMapper 在我们希望使用 spring jdbc 急切获取详细信息的主/详细场景中的最佳实践用法。
假设我们同时拥有 Invoice 和 InvoiceLine 类。
public class Invoice
private BigDecimal invId;
private Date invDate;
private List<InvoiceLine> lines;
public class InvoiceLine
private int order;
private BigDecimal price;
private BigDecimal quantity;
当使用带有行映射器的 Spring Jdbc 时,我们通常有一个
public class InvoiceMapper implements RowMapper<Invoice>
public Invoice mapRow(ResultSet rs, int rowNum) throws SQLException
Invoice invoice = new Invoice();
invoice.setInvId(rs.getBigDecimal("INVID"));
invoice.setInvDate(rs.getDate("INVDATE"));
return invoice;
现在的问题是我想急切地获取与此发票实例相关的 InvoiceLine。 如果我在 rowmapper 类中查询数据库可以吗?还是有人喜欢另一种方式?我使用下面的模式,但对此并不满意。
public class InvoiceMapper implements RowMapper<Invoice>
private JdbcTemplate jdbcTemplate;
private static final String SQLINVLINE=
"SELECT * FROM INVOICELINES WHERE INVID = ?";
public Invoice mapRow(ResultSet rs, int rowNum) throws SQLException
Invoice invoice = new Invoice();
invoice.setInvId(rs.getBigDecimal("INVID"));
invoice.setInvDate(rs.getDate("INVDATE"));
invoice.setLines(jdbcTemplate.query(SQLINVLINE,
new Object[]invoice.getInvId,new InvLineMapper());
return invoice;
我感觉这种方法有问题,但找不到更好的方法。如果有人能告诉我为什么这是一个糟糕的设计,如果是这样,那么正确的用法是什么,我会非常高兴。
【问题讨论】:
请在您的代码中写出完整的单词。你不会用完硬盘空间。我保证。 【参考方案1】:您在这里重新创建了1 + n
问题。
要解决它,您需要使用将外部查询更改为连接,然后手工制作一个循环来将平面连接结果集解析为您的 Invoice 1 -> * InvLine
List<Invoice> results = new ArrayList<>();
jdbcTemplate.query("SELECT * FROM INVOICE inv JOIN INVOICE_LINE line on inv.id = line.invoice_id", null,
new RowCallbackHandler()
private Invoice current = null;
private InvoiceMapper invoiceMapper ;
private InvLineMapper lineMapper ;
public void processRow(ResultSet rs)
if ( current == null || rs.getInt("inv.id") != current.getId() )
current = invoiceMapper.mapRow(rs, 0); // assumes rownum not important
results.add(current);
current.addInvoiceLine( lineMapper.mapRow(rs, 0) );
我显然还没有编译过这个......希望你明白这一点。还有另一种选择,使用休眠或任何 JPA 实现,它们开箱即用,将为您节省大量时间。
更正:确实应该使用@gkamal 在他的回答中使用的ResultSetExtractor
,但总体逻辑仍然存在。
【讨论】:
+1 用于休眠/jpa,如果是一次性的情况,可以使用 jdbcTemplate。如果同样的事情出现在很多地方,你最好使用 ORM。【参考方案2】:ResultSetExtractor 是执行此操作的更好选择。执行一个连接两个表的查询,然后遍历结果集。您将需要一些逻辑来聚合属于同一发票的多行 - 通过按发票 ID 排序并检查 ID 何时更改或使用如下示例所示的映射。
jdbcTemplate.query("SELECT * FROM INVOICE inv JOIN INVOICE_LINE line " +
+ " on inv.id = line.invoice_id", new ResultSetExtractor<List<Invoice>>()
public List<Invoice> extractData(ResultSet rs)
Map<Integer,Invoice> invoices = new HashMap<Integer,Invoice>();
while(rs.hasNext())
rs.next();
Integer invoiceId = rs.getInt("inv.id");
Invoice invoice = invoces.get(invoiceId);
if (invoice == null)
invoice = invoiceRowMapper.mapRow(rs);
invoices.put(invoiceId,invoice);
InvoiceItem item = invLineMapper.mapRow(rs);
invoice.addItem(item);
return invoices.values();
);
【讨论】:
感谢您的解决方案。我可以知道您是否建议将此作为替代选项,或者这是正确的做法(避免在 rowmapper/resultsetextractor 类中使用 sql 代码) 是的,这是一种更好的方法。在映射器中执行查询 - 尤其是针对对象列表时会很慢。 @gkamal 你能帮我看看这个问题吗?我需要一些建议***.com/questions/15555163/… 我正在查看你的答案,我试图将它应用到我的场景中我想知道我是否必须使用maprow 和 resultsetextractor 的组合?【参考方案3】:基于ResultSetExtractor
的公认解决方案可以变得更加模块化和可重用:在我的应用程序中,我创建了一个CollectingRowMapper
接口和一个抽象实现。请参见下面的代码,它包含 Javadoc cmets。
CollectingRowMapper 接口:
import org.springframework.jdbc.core.RowMapper;
/**
* A RowMapper that collects data from more than one row to generate one result object.
* This means that, unlike normal RowMapper, a CollectingRowMapper will call
* <code>next()</code> on the given ResultSet until it finds a row that is not related
* to previous ones. Rows <b>must be sorted</b> so that related rows are adjacent.
* Tipically the T object will contain some single-value property (an id common
* to all collected rows) and a Collection property.
* <p/>
* NOTE. Implementations will be stateful (to save the result of the last call
* to <code>ResultSet.next()</code>), so <b>they cannot have singleton scope</b>.
*
* @see AbstractCollectingRowMapper
*
* @author Pino Navato
**/
public interface CollectingRowMapper<T> extends RowMapper<T>
/**
* Returns the same result of the last call to <code>ResultSet.next()</code> made by <code>RowMapper.mapRow(ResultSet, int)</code>.
* If <code>next()</code> has not been called yet, the result is meaningless.
**/
public boolean hasNext();
抽象实现类:
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Basic implementation of @link CollectingRowMapper.
*
* @author Pino Navato
**/
public abstract class AbstractCollectingRowMapper<T> implements CollectingRowMapper<T>
private boolean lastNextResult;
@Override
public T mapRow(ResultSet rs, int rowNum) throws SQLException
T result = mapRow(rs, null, rowNum);
while (nextRow(rs) && isRelated(rs, result))
result = mapRow(rs, result, ++rowNum);
return result;
/**
* Collects the current row into the given partial result.
* On the first call partialResult will be null, so this method must create
* an instance of T and map the row on it, on subsequent calls this method updates
* the previous partial result with data from the new row.
*
* @return The newly created (on the first call) or modified (on subsequent calls) partialResult.
**/
protected abstract T mapRow(ResultSet rs, T partialResult, int rowNum) throws SQLException;
/**
* Analyzes the current row to decide if it is related to previous ones.
* Tipically it will compare some id on the current row with the one stored in the partialResult.
**/
protected abstract boolean isRelated(ResultSet rs, T partialResult) throws SQLException;
@Override
public boolean hasNext()
return lastNextResult;
protected boolean nextRow(ResultSet rs) throws SQLException
lastNextResult = rs.next();
return lastNextResult;
ResultSetExtractor 实现:
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.util.Assert;
/**
* A ResultSetExtractor that uses a CollectingRowMapper.
* This class has been derived from the source code of Spring's RowMapperResultSetExtractor.
*
* @author Pino Navato
**/
public class CollectingRowMapperResultSetExtractor<T> implements ResultSetExtractor<List<T>>
private final CollectingRowMapper<T> rowMapper;
private final int rowsExpected;
/**
* Create a new CollectingRowMapperResultSetExtractor.
* @param rowMapper the RowMapper which creates an object for each row
**/
public CollectingRowMapperResultSetExtractor(CollectingRowMapper<T> rowMapper)
this(rowMapper, 0);
/**
* Create a new CollectingRowMapperResultSetExtractor.
* @param rowMapper the RowMapper which creates an object for each row
* @param rowsExpected the number of expected rows (just used for optimized collection handling)
**/
public CollectingRowMapperResultSetExtractor(CollectingRowMapper<T> rowMapper, int rowsExpected)
Assert.notNull(rowMapper, "RowMapper is required");
this.rowMapper = rowMapper;
this.rowsExpected = rowsExpected;
@Override
public List<T> extractData(ResultSet rs) throws SQLException
List<T> results = (rowsExpected > 0 ? new ArrayList<>(rowsExpected) : new ArrayList<>());
int rowNum = 0;
if (rs.next())
do
results.add(rowMapper.mapRow(rs, rowNum++));
while (rowMapper.hasNext());
return results;
上面的所有代码都可以作为库重用。您只需继承AbstractCollectingRowMapper
并实现这两个抽象方法。
使用示例:
给定如下查询:
SELECT * FROM INVOICE inv
JOIN INVOICELINES lines
on inv.INVID = lines.INVOICE_ID
order by inv.INVID
您可以只为两个连接表编写一个映射器:
public class InvoiceRowMapper extends AbstractCollectingRowMapper<Invoice>
@Override
protected Invoice mapRow(ResultSet rs, Invoice partialResult, int rowNum) throws SQLException
if (partialResult == null)
partialResult = new Invoice();
partialResult.setInvId(rs.getBigDecimal("INVID"));
partialResult.setInvDate(rs.getDate("INVDATE"));
partialResult.setLines(new ArrayList<>());
InvoiceLine line = new InvoiceLine();
line.setOrder(rs.getInt("ORDER"));
line.setPrice(rs.getBigDecimal("PRICE"));
line.setQuantity(rs.getBigDecimal("QUANTITY"));
partialResult.getLines().add(line);
return partialResult;
/** Returns true if the current record has the same invoice ID of the previous ones. **/
@Override
protected boolean isRelated(ResultSet rs, Invoice partialResult) throws SQLException
return partialResult.getInvId().equals(rs.getBigDecimal("INVID"));
最后说明:我主要在 Spring Batch 中使用 CollectingRowMapper
和 AbstractCollectingRowMapper
,在 JdbcCursorItemReader
的自定义子类中:我在 another answer 中描述了这个解决方案。使用 Spring Batch,您可以在获取下一组相关行之前处理每组相关行,因此您可以避免加载可能很大的整个查询结果。
【讨论】:
【参考方案4】:最简单的方法
您可以简单地使用这个库 - SimpleFlatMapper 已经解决了这个问题。您需要做的就是使用JdbcTemplateMapperFactory
创建一个ResultSetExtractor
。
import org.simpleflatmapper.jdbc.spring.JdbcTemplateMapperFactory;
private final ResultSetExtractor<List<Invoice>> resultSetExtractor =
JdbcTemplateMapperFactory
.newInstance()
.addKeys("id") // the column name you expect the invoice id to be on
.newResultSetExtractor(Invoice.class);
String query = "SELECT * FROM INVOICE inv JOIN INVOICE_LINE line on inv.id = line.invoice_id"
List<Invoice> results = jdbcTemplate.query(query, resultSetExtractor);
将此依赖项添加到pom.xml
<dependency>
<groupId>org.simpleflatmapper</groupId>
<artifactId>sfm-springjdbc</artifactId>
<version>8.2.1</version>
</dependency>
这里有一篇文章可以参考-https://arnaudroger.github.io/blog/2017/06/13/jdbc-template-one-to-many.html
以下是不同用法的示例 - https://github.com/arnaudroger/SimpleFlatMapper/blob/master/sfm-springjdbc/src/test/java/org/simpleflatmapper/jdbc/spring/test/JdbcTemplateMapperFactoryTest.java
【讨论】:
以上是关于Spring JDBC RowMapper 用于急切获取的主要内容,如果未能解决你的问题,请参考以下文章
一个加速产生 Spring JDBC RowMapper 的工具类
Spring Framework应用中JDBC使用RowMapper的一些疑惑
使用 Spring JDBC Rowmapper 映射分层数据
基于spring-boot-data-jdbc的RowMapper实例的初始化配置,配置内容和实体类的代码由测试类生成
spring对JDBC整合的时候,要注意自己写一个映射,继承RowMapper,查询的时候会用到,将返回的ResultSet一条对应一个Employee对象。