복붙노트

[SPRING] eager fetches를위한 Spring JDBC RowMapper 사용법

SPRING

eager fetches를위한 Spring JDBC RowMapper 사용법

문제는 Spring jdbc를 사용하여 세부 사항을 열심히 가져오고 자하는 마스터 / 세부 시나리오의 RowMapper에 대한 모범 사례 사용에 관한 것입니다.

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.이렇게하기 위해서는 ResultSetExtractor가 더 좋습니다. 두 테이블을 조인 한 쿼리를 실행 한 다음 결과 집합을 반복합니다. 인보이스 ID로 주문하고 ID가 변경 될 때 확인하거나 아래 예와 같이지도를 사용하여 동일한 인보이스에 속하는 여러 행을 집계하는 논리가 필요합니다.

    이렇게하기 위해서는 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();
        }
    
    
    });
    
  2. ==============================

    2.여기서 1 + n 문제를 재현 한 것입니다.

    여기서 1 + n 문제를 재현 한 것입니다.

    이를 해결하려면 외부 쿼리를 조인으로 변경 한 다음 공예품에 루프를 보내어 플랫 조인 결과 세트를 인보이스로 구문 분석해야합니다. 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이 그의 대답에 사용 했어야하지만, 모든 논리가 여전히 유효합니다.

  3. ==============================

    3.ResultSetExtractor는 전체 ResultSet을 처리하고 하나의 (거대한 거대한) 객체를 반환하기 때문에 사용하지 않는 것이 좋습니다.

    ResultSetExtractor는 전체 ResultSet을 처리하고 하나의 (거대한 거대한) 객체를 반환하기 때문에 사용하지 않는 것이 좋습니다.

    내 솔루션을 사용하면 관련 행 그룹당 하나의 객체를 얻을 수 있으며 다음 객체를 얻기 전에이 객체를 처리 할 수 ​​있습니다. 응용 프로그램에서 CollectingRowMapper 인터페이스와 추상 구현을 만들었습니다. 아래 코드를 참조하십시오. Javadoc 주석이 들어 있습니다.

    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 must be sorted 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;
        }
    }
    

    JdbcCursorItemReader의 커스텀 서브 클래스에서 Spring Batch와 함께이 코드를 사용한다. 나는이 해답을 또 다른 대답으로 설명했다.

  4. from https://stackoverflow.com/questions/11455220/spring-jdbc-rowmapper-usage-for-eager-fetches by cc-by-sa and MIT license