14

问题是关于 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;
    }
}

我觉得这种方法有问题,但找不到更好的方法。如果有人能告诉我为什么这是一个糟糕的设计,我会非常高兴,如果是这样,正确的用法是什么。

4

4 回答 4

18

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


});
于 2012-07-12T17:11:11.463 回答
7

公认的基于 的解决方案ResultSetExtractor可以变得更加模块化和可重用:在我的应用程序中,我创建了一个CollectingRowMapper接口和一个抽象实现。请参阅下面的代码,它包含 Javadoc 注释。

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:我在另一个答案中描述了这个解决方案。使用 Spring Batch,您可以在获取下一组相关行之前处理每组相关行,因此您可以避免加载可能很大的整个查询结果。AbstractCollectingRowMapperJdbcCursorItemReader

于 2017-09-21T14:01:02.213 回答
4

你在这里重现的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 实现,他们开箱即用地做这种事情,将为您节省大量时间。

更正:应该真正使用ResultSetExtractor@gkamal在他的答案中使用的,但总体逻辑仍然存在。

于 2012-07-12T15:56:40.577 回答
0

最简单的方法

你可以简单地使用这个库——SimpleFlatMapper已经解决了这个问题。您需要做的就是ResultSetExtractor使用JdbcTemplateMapperFactory.

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

于 2020-04-09T05:32:45.667 回答