【Spring】利用spring的JdbcTemplate查詢返回結果映射到自定義類型


// org.springframework.jdbc.core.JdbcTemplate 中的查詢方法基本都有支持參數RowMapper<T> rowMapper的重載方法。下面只是隨便舉例2個,還有很多

public <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
    ...
};


public <T> T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
    ...
};

//demo01
List<Person> person = jdbcTemplate.query(sql, new RowMapper<Person>() {    
    @Override    
    public Person mapRow(ResultSet rs, int i) throws SQLException {        
        Person p = new Person(); //特別需要new,不然如果結果集是list就只有1個對象        
         p.setId(rs.getString("id"));        
        return p;    
    }});

//特別 如果如demo寫,很麻煩要set很多。此時spring提供了一個RowMapper的實現類BeanPropertyRowMapper
//demo02
List<Person> person =  jdbcTemplate.query(sql, new BeanPropertyRowMapper(Person.class));

這篇博客的主要目的是分析BeanPropertyRowMapper的實現是怎么樣。

先,之前也在用jdbcTemplate來查詢,但都是用demo01的方式。上周末本來想寫一個BaseRowMapper(其實就是BeanPropertyRowMapper),但冬(lan)眠(si)去了。

在今天(2016-11-07)上班的時候又用到了,於是就打算寫。但看了下RowMapper的結構(ctrl+t)發現了BeanPropertyRowMapper。這不就是我想要的嗎,於是決定去看下和自己的想法有什么差別。

現在,我還沒看過源碼,先說我周末大致想到的:

  1、肯定要用反射,根據sql的列名/別名去找到對應的set;

    以sql返回結果集的列為准,sql有的必須有set,有set不一定sql有返回。

  2、反射效率低,如果我sql返回的是list,不應該每行都要根據反射去找set。而是應該在第一次找的時候,把列名/別名對應的set緩存起來,以后直接取;

    第一次(第一行結果)用列名/別名(不區分大小寫),找到set,並緩存;之后直接用別名/列名去找set,節約反射查找set消耗的時間。

注:以下都是BeanPropertyRowMapper源碼分析

一、緩存自定義類型的set方法

// BeanPropertyRowMapper的成員變量

/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());

/** The class we are mapping to ;要映射的class*/
private Class<T> mappedClass;

/** Whether we're strictly validating; 是否嚴格映射bean和sql結果 */
private boolean checkFullyPopulated = false;

/** Whether we're defaulting primitives when mapping a null value */
private boolean primitivesDefaultedForNullValue = false;

/** Map of the fields we provide mapping for;映射字段的set方法 */
private Map<String, PropertyDescriptor> mappedFields;

/** Set of bean properties we provide mapping for ;需要映射的字段*/
private Set<String> mappedProperties;
/**
 * Create a new BeanPropertyRowMapper, accepting unpopulated properties in the target bean.
 * <p>Consider using the {@link #newInstance} factory method instead,which allows for specifying the mapped type once only.
 * @param mappedClass the class that each row should be mapped to
 */
public BeanPropertyRowMapper(Class<T> mappedClass) {
    initialize(mappedClass);
}

/**
 * Create a new BeanPropertyRowMapper.
 * @param mappedClass the class that each row should be mapped to
 * @param checkFullyPopulated whether we're strictly validating that all bean properties have been mapped from corresponding database fields
 */
public BeanPropertyRowMapper(Class<T> mappedClass, boolean checkFullyPopulated) {
    initialize(mappedClass);
    this.checkFullyPopulated = checkFullyPopulated; //是否嚴格驗證,所有bean屬性已經從對應的數據庫字段映射。
}

BeanPropertyRowMapper提供的2中構造函數中,區別只在於是否嚴格映射bean和sql結果(默認是false,不嚴格映射)。

/**
 * Initialize the mapping metadata for the given class.
 * @param mappedClass the mapped class.
 */
protected void initialize(Class<T> mappedClass) {
    this.mappedClass = mappedClass;  
    this.mappedFields = new HashMap<String, PropertyDescriptor>(); 
    this.mappedProperties = new HashSet<String>();
    //以上都是設置/初始化成員變量

    PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);//org.springframework.beans.BeanUtils
   for (PropertyDescriptor pd : pds) {
        if (pd.getWriteMethod() != null) {
            this.mappedFields.put(pd.getName().toLowerCase(), pd); // key:全小寫 
              String underscoredName = underscoreName(pd.getName()); // ex:bookName --> book_name
            if (!pd.getName().toLowerCase().equals(underscoredName)) {
              //set與其屬性命名的不一致;方法是setBookName 而變量是book_name; 大致是這意思
                this.mappedFields.put(underscoredName, pd);
            }
            this.mappedProperties.add(pd.getName()); //key:與mappedFields不一樣
        }
    }
}

/**
 * Convert a name in camelCase to an underscored name in lower case.
 * Any upper case letters are converted to lower case with a preceding underscore.
 * @param name the string containing original name
 * @return the converted name
 */
private String underscoreName(String name) { //ex: bookName --> book_name
    if (!StringUtils.hasLength(name)) {
        return "";
    }
    StringBuilder result = new StringBuilder();
    result.append(name.substring(0, 1).toLowerCase());
    for (int i = 1; i < name.length(); i++) {
        String s = name.substring(i, i + 1);
        String slc = s.toLowerCase();
        if (!s.equals(slc)) { //大寫字母轉換成  _+小寫
            result.append("_").append(slc);
        }
        else {
            result.append(s);
        }
    }
    return result.toString();
}

注意:

  1、Map<String, PropertyDescriptor> mappedFields的key與Set<String> mappedProperties的value保存的並不一定是一樣的:

    mappedFields的key是set方法的全小寫/帶下划線的全小寫,而mappedProperties的是set方法名。

    ex: private String bookName; public void setBookName(..)

          mappedFields:bookname/book_name   mappedProperties:bookName

  2、關於underscoreName()的轉換,效果就是: 大寫 –> _+小寫。 初略的認為是轉換成員變量與對應set命名不一樣的問題。

BeanPropertyRowMapper.initialize(…)結合自己的設想:

  1、先根據class緩存了所有的set方法,並保存在了mappedFields。

    即當初我想要的效果,不過我想的可能是在執行第一次的時候(mapRow方法中)。而BeanPropertyRowMapper則是在構造的時候就緩存了。

  2、我所沒想到的underscoreName(),可能我項目並沒存在命名問題。成員變量的set/get都是工具自己生成的,命名也是采取的駝峰式(不管是java還是sql的別名)

  3、對於PropertyDescriptor的獲取,spring還是用了自己的獲取。我的話不確定,畢竟對反射也不是很熟悉。

    但看過一篇文章: java反射的性能問題 ,如果我自己寫的話,估計還是會用java自帶的吧。

    雖然不清楚spring和java自帶的區別及效率,但我覺得應該spring比較好吧。不然spring直接用jdk的就行了,沒必要自己再寫。

以上,BeanPropertyRowMapper在構造的時候已經有了反射,接下來就是把每行的值寫到對應的屬性中。

二、寫入sql結果集的值到對應bean屬性

// jdbcTemplate調用RowMapper.mapRow(...) 
public class RowMapperResultSetExtractor<T> implements ResultSetExtractor<List<T>> {

    private final RowMapper<T> rowMapper;

    private final int rowsExpected;


    /**
     * Create a new RowMapperResultSetExtractor.
     * @param rowMapper the RowMapper which creates an object for each row
     */
    public RowMapperResultSetExtractor(RowMapper<T> rowMapper) {
        this(rowMapper, 0);
    }

    /**
     * Create a new RowMapperResultSetExtractor.
     * @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 RowMapperResultSetExtractor(RowMapper<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 = (this.rowsExpected > 0 ? new ArrayList<T>(this.rowsExpected) : new ArrayList<T>());
        int rowNum = 0;
        while (rs.next()) {
            results.add(this.rowMapper.mapRow(rs, rowNum++));//調用核心; 1、每行的rowMapper是同一個對象,所以可以緩存映射關系 2、mapRow為什么是new對象也是因為這個。不然list.add的是同一個return對象。
        }
        return results;
    }
}
// BeanPropertyRowMapper中mapRow的實現    
@Override
public T mapRow(ResultSet rs, int rowNumber) throws SQLException {
    Assert.state(this.mappedClass != null, "Mapped class was not specified");
    T mappedObject = BeanUtils.instantiate(this.mappedClass);  //實例化一個新對象;就是class.newInstance();
 
    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject); //這也是spring自己的,有興趣可以看。在這主要就是類似method.invoke(…)
    initBeanWrapper(bw); //這是個空方法,用於子類擴展
     ResultSetMetaData rsmd = rs.getMetaData();
    int columnCount = rsmd.getColumnCount();  // 與rsmd都是sql結果集的信息
     Set<String> populatedProperties = (isCheckFullyPopulated() ? new HashSet<String>() : null);//是否嚴格映射bean和sql

    for (int index = 1; index <= columnCount; index++) {
       String column = JdbcUtils.lookupColumnName(rsmd, index);  // 得到sql的列名/別名
        PropertyDescriptor pd = this.mappedFields.get(column.replaceAll(" ", "").toLowerCase());  // 從緩存中得到方法信息
        if (pd != null) {
            try {
                Object value = getColumnValue(rs, index, pd);  // 得到每列的值。為什么要pd:因為要根據類型獲取相應的值。
                if (logger.isDebugEnabled() && rowNumber == 0) {
                    logger.debug("Mapping column '" + column + "' to property '" +
                            pd.getName() + "' of type " + pd.getPropertyType());
                }
                try {
                    bw.setPropertyValue(pd.getName(), value);  // 設置結果
                 }
                catch (TypeMismatchException e) {
                    if (value == null && primitivesDefaultedForNullValue) {
                        logger.debug("Intercepted TypeMismatchException for row " + rowNumber +
                                " and column '" + column + "' with value " + value +
                                " when setting property '" + pd.getName() + "' of type " + pd.getPropertyType() +
                                " on object: " + mappedObject);
                    }
                    else {
                        throw e;
                    }
                }
                if (populatedProperties != null) { //嚴格映射的邏輯判斷
                    populatedProperties.add(pd.getName());
                }
            }
            catch (NotWritablePropertyException ex) {
                throw new DataRetrievalFailureException(
                        "Unable to map column " + column + " to property " + pd.getName(), ex);
            }
        }
    }

    if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) { //嚴格映射的邏輯判斷 
        throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " +
                "necessary to populate object of class [" + this.mappedClass + "]: " + this.mappedProperties);
    }

    return mappedObject;
}
public static <T> T instantiate(Class<T> clazz) throws BeanInstantiationException {
        Assert.notNull(clazz, "Class must not be null");
        if (clazz.isInterface()) {
            throw new BeanInstantiationException(clazz, "Specified class is an interface");
        }
        try {
            return clazz.newInstance();
        }
        catch (InstantiationException ex) {
            throw new BeanInstantiationException(clazz, "Is it an abstract class?", ex);
        }
        catch (IllegalAccessException ex) {
            throw new BeanInstantiationException(clazz, "Is the constructor accessible?", ex);
        }
    }
/**
 * Initialize the given BeanWrapper to be used for row mapping.
 * To be called for each row.
 * <p>The default implementation is empty. Can be overridden in subclasses.
 * @param bw the BeanWrapper to initialize
 */
protected void initBeanWrapper(BeanWrapper bw) {
}
protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException {
    return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType());
}

// JdbcUtils.class   根據set參數的類型,決定sql返回值調用類型
public static Object getResultSetValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException {
    if (requiredType == null) {
        return getResultSetValue(rs, index);
    }

    Object value;

    // Explicitly extract typed value, as far as possible.
    if (String.class.equals(requiredType)) {
        return rs.getString(index);
    }
    else if (boolean.class.equals(requiredType) || Boolean.class.equals(requiredType)) {
        value = rs.getBoolean(index);
    }
    else if (byte.class.equals(requiredType) || Byte.class.equals(requiredType)) {
        value = rs.getByte(index);
    }
    else if (short.class.equals(requiredType) || Short.class.equals(requiredType)) {
        value = rs.getShort(index);
    }
    else if (int.class.equals(requiredType) || Integer.class.equals(requiredType)) {
        value = rs.getInt(index);
    }
    else if (long.class.equals(requiredType) || Long.class.equals(requiredType)) {
        value = rs.getLong(index);
    }
    else if (float.class.equals(requiredType) || Float.class.equals(requiredType)) {
        value = rs.getFloat(index);
    }
    else if (double.class.equals(requiredType) || Double.class.equals(requiredType) ||
            Number.class.equals(requiredType)) {
        value = rs.getDouble(index);
    }
    else if (BigDecimal.class.equals(requiredType)) {
        return rs.getBigDecimal(index);
    }
    else if (java.sql.Date.class.equals(requiredType)) {
        return rs.getDate(index);
    }
    else if (java.sql.Time.class.equals(requiredType)) {
        return rs.getTime(index);
    }
    else if (java.sql.Timestamp.class.equals(requiredType) || java.util.Date.class.equals(requiredType)) {
        return rs.getTimestamp(index);
    }
    else if (byte[].class.equals(requiredType)) {
        return rs.getBytes(index);
    }
    else if (Blob.class.equals(requiredType)) {
        return rs.getBlob(index);
    }
    else if (Clob.class.equals(requiredType)) {
        return rs.getClob(index);
    }
    else {
        // Some unknown type desired -> rely on getObject.
        if (getObjectWithTypeAvailable) {
            try {
                return rs.getObject(index, requiredType);
            }
            catch (AbstractMethodError err) {
                logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err);
            }
            catch (SQLFeatureNotSupportedException ex) {
                logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex);
            }
            catch (SQLException ex) {
                logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex);
            }
        }
        // Fall back to getObject without type specification...
        return getResultSetValue(rs, index);
    }

    // Perform was-null check if necessary (for results that the JDBC driver returns as primitives).
    return (rs.wasNull() ? null : value);
}

基本核心的全部源碼就在這;但有個核心沒看也沒寫出源碼就是:

  BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
  bw.setPropertyValue(pd.getName(), value);

其目的個人理解就是:method.invoke(…)

感覺總體來說,和我的設想是一樣的。但只是實現上效率的區別。就在PropertyDescriptor、Method、及set調用。

個人話應該不會去寫PropertyDescriptor的獲取,就直接用jdk提供的了。而set的調用也是一樣,直接用PropertyDescriptor找到set的Method,然后Method.invoke(…);

(所以個人最初想的時候緩存的其實是Method而不是PropertyDescriptor)

值的獲取也沒太大區別,都是判斷set的參數類型,然后調用ResultSet的對應方法。

 

總結:

  考慮到了優化最明顯的查找set方法小號。但其余的優化還不知道:不知道哪些還可以優化、怎么優化,有待學習。

(有待不知是哪年了,周圍的都沒注重技術的。都只要求業務功能的代碼,管你怎么寫的,也不願給你時間、資源去學習,就知道催進度。)

 

ps:

  哎,回到家大概19.50。從8.10來分開始邊寫邊看BeanPropertyRowMapper,感覺也沒寫什么而且那源碼核心的復雜的BeanWrapper還沒看。單獨是BeanPropertyRowMapper其實幾分鍾就看完了。結果寫完就22.40左右了。

  要說學到了什么嗎,真的不知道。項目實際接觸的都是業務代碼,沒有任何技術可言(不管是舊技術還是新技術)。

  心好累…一直想轉行,可惜一無是處,哎!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM