
com.ktanx.jdbc.springjdbc.persist.JdbcRowMapper Maven / Gradle / Ivy
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ktanx.jdbc.springjdbc.persist;
import com.ktanx.common.utils.NameUtils;
import com.ktanx.jdbc.annotation.Column;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.*;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;
/**
* 只是简单复制重写了spring的{@link BeanPropertyRowMapper}.
* 本来只需要重写initialize方法,但是在BeanPropertyRowMapper中虽然该方法是protected允许重写,但是里面用到的属性却是private
* 所以只能复制重写了整个类.
*
* @author Thomas Risberg
* @author Juergen Hoeller
* @since 2.5
*/
public class JdbcRowMapper implements RowMapper {
/**
* Logger available to subclasses
*/
protected final Log logger = LogFactory.getLog(getClass());
/**
* The class we are mapping to
*/
private Class mappedClass;
/**
* Whether we're strictly validating
*/
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
*/
private Map mappedFields;
/**
* Set of bean properties we provide mapping for
*/
private Set mappedProperties;
/**
* Create a new {@code BeanPropertyRowMapper} for bean-style configuration.
*
* @see #setMappedClass
* @see #setCheckFullyPopulated
*/
public JdbcRowMapper() {
}
/**
* Create a new {@code BeanPropertyRowMapper}, accepting unpopulated
* properties in the target bean.
* 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 JdbcRowMapper(Class mappedClass) {
initialize(mappedClass);
}
/**
* Create a new {@code 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 JdbcRowMapper(Class mappedClass, boolean checkFullyPopulated) {
initialize(mappedClass);
this.checkFullyPopulated = checkFullyPopulated;
}
/**
* Set the class that each row should be mapped to.
*/
public void setMappedClass(Class mappedClass) {
if (this.mappedClass == null) {
initialize(mappedClass);
} else {
if (this.mappedClass != mappedClass) {
throw new InvalidDataAccessApiUsageException("The mapped class can not be reassigned to map to "
+ mappedClass
+ " since it is already providing mapping for "
+ this.mappedClass);
}
}
}
/**
* Get the class that we are mapping to.
*/
public final Class getMappedClass() {
return this.mappedClass;
}
/**
* Set whether we're strictly validating that all bean properties have been mapped
* from corresponding database fields.
* Default is {@code false}, accepting unpopulated properties in the target bean.
*/
public void setCheckFullyPopulated(boolean checkFullyPopulated) {
this.checkFullyPopulated = checkFullyPopulated;
}
/**
* Return whether we're strictly validating that all bean properties have been
* mapped from corresponding database fields.
*/
public boolean isCheckFullyPopulated() {
return this.checkFullyPopulated;
}
/**
* Set whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
*
Default is {@code false}, throwing an exception when nulls are mapped to Java primitives.
*/
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
}
/**
* Return whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
*/
public boolean isPrimitivesDefaultedForNullValue() {
return this.primitivesDefaultedForNullValue;
}
/**
* Initialize the mapping metadata for the given class.
*
* @param mappedClass the mapped class
*/
protected void initialize(Class mappedClass) {
this.mappedClass = mappedClass;
this.mappedFields = new HashMap();
this.mappedProperties = new HashSet();
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);
for (PropertyDescriptor pd : pds) {
if (pd.getWriteMethod() != null) {
Method readMethod = pd.getReadMethod();
if (readMethod != null) {
Column aColumn = readMethod.getAnnotation(Column.class);
if (aColumn != null) {
String name = NameUtils.getLegalName(aColumn.value());
this.mappedFields.put(lowerCaseName(name), pd);
}
}
this.mappedFields.put(lowerCaseName(pd.getName()), pd);
String underscoredName = underscoreName(pd.getName());
if (!lowerCaseName(pd.getName()).equals(underscoredName)) {
this.mappedFields.put(underscoredName, pd);
}
this.mappedProperties.add(pd.getName());
}
}
}
/**
* 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 original name
* @return the converted name
* @see #lowerCaseName
* @since 4.2
*/
protected String underscoreName(String name) {
if (!StringUtils.hasLength(name)) {
return "";
}
StringBuilder result = new StringBuilder();
result.append(lowerCaseName(name.substring(0, 1)));
for (int i = 1; i < name.length(); i++) {
String s = name.substring(i, i + 1);
String slc = lowerCaseName(s);
if (!s.equals(slc)) {
result.append("_").append(slc);
} else {
result.append(s);
}
}
return result.toString();
}
/**
* Convert the given name to lower case.
* By default, conversions will happen within the US locale.
*
* @param name the original name
* @return the converted name
* @since 4.2
*/
protected String lowerCaseName(String name) {
return name.toLowerCase(Locale.US);
}
/**
* Extract the values for all columns in the current row.
* Utilizes public setters and result set metadata.
*
* @see ResultSetMetaData
*/
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);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
initBeanWrapper(bw);
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
Set populatedProperties = (isCheckFullyPopulated() ? new HashSet() : null);
for (int index = 1; index <= columnCount; index++) {
String column = JdbcUtils.lookupColumnName(rsmd, index);
String field = lowerCaseName(column.replaceAll(" ", ""));
PropertyDescriptor pd = this.mappedFields.get(field);
if (pd != null) {
try {
Object value = getColumnValue(rs, index, pd);
if (rowNumber == 0 && logger.isDebugEnabled()) {
logger.debug("Mapping column '" + column + "' to property '" + pd.getName() + "' of type ["
+ ClassUtils.getQualifiedName(pd.getPropertyType()) + "]");
}
try {
bw.setPropertyValue(pd.getName(), value);
} catch (TypeMismatchException ex) {
if (value == null && this.primitivesDefaultedForNullValue) {
if (logger.isDebugEnabled()) {
logger.debug("Intercepted TypeMismatchException for row " + rowNumber + " and column '"
+ column + "' with null value when setting property '" + pd.getName()
+ "' of type [" + ClassUtils.getQualifiedName(pd.getPropertyType())
+ "] on object: " + mappedObject, ex);
}
} else {
throw ex;
}
}
if (populatedProperties != null) {
populatedProperties.add(pd.getName());
}
} catch (NotWritablePropertyException ex) {
throw new DataRetrievalFailureException("Unable to map column '" + column + "' to property '"
+ pd.getName() + "'", ex);
}
} else {
// No PropertyDescriptor found
if (rowNumber == 0 && logger.isDebugEnabled()) {
logger.debug("No property found for column '" + column + "' mapped to field '" + field + "'");
}
}
}
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.getName() + "]: " + this.mappedProperties);
}
return mappedObject;
}
/**
* Initialize the given BeanWrapper to be used for row mapping.
* To be called for each row.
* The default implementation is empty. Can be overridden in subclasses.
*
* @param bw the BeanWrapper to initialize
*/
protected void initBeanWrapper(BeanWrapper bw) {
}
/**
* Retrieve a JDBC object value for the specified column.
*
The default implementation calls
* {@link JdbcUtils#getResultSetValue(ResultSet, int, Class)}.
* Subclasses may override this to check specific value types upfront,
* or to post-process values return from {@code getResultSetValue}.
*
* @param rs is the ResultSet holding the data
* @param index is the column index
* @param pd the bean property that each result object is expected to match
* (or {@code null} if none specified)
* @return the Object value
* @throws SQLException in case of extraction failure
* @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(ResultSet, int, Class)
*/
protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException {
return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType());
}
/**
* Static factory method to create a new {@code BeanPropertyRowMapper}
* (with the mapped class specified only once).
*
* @param mappedClass the class that each row should be mapped to
*/
public static JdbcRowMapper newInstance(Class mappedClass) {
return new JdbcRowMapper(mappedClass);
}
}