com.sonsure.dumper.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.sonsure.dumper.springjdbc.persist;
import com.sonsure.dumper.core.config.JdbcEngineConfig;
import com.sonsure.dumper.core.convert.JdbcTypeConverterComposite;
import com.sonsure.dumper.core.mapping.MappingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.beans.PropertyDescriptor;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;
/**
* 重写了spring的{@link BeanPropertyRowMapper}.
* 在原来的基础上添加了自定义映射以及注解的支持
*
* @author Thomas Risberg
* @author Juergen Hoeller
* @since 2.5
*/
public class JdbcRowMapper implements RowMapper {
/**
* Logger available to subclasses
*/
protected final Logger logger = LoggerFactory.getLogger(getClass());
private String dialect;
private JdbcEngineConfig jdbcEngineConfig;
/**
* 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 #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 dialect the dialect
* @param jdbcEngineConfig the jdbc engine config
* @param mappedClass the class that each row should be mapped to
*/
public JdbcRowMapper(String dialect, JdbcEngineConfig jdbcEngineConfig, Class mappedClass) {
this.dialect = dialect.toLowerCase();
this.jdbcEngineConfig = jdbcEngineConfig;
this.mappedClass = mappedClass;
this.mappedFields = new HashMap<>();
this.mappedProperties = new HashSet<>();
initialize();
}
/**
* 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;
}
/**
* Initialize the mapping metadata for the given class.
*/
protected void initialize() {
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);
for (PropertyDescriptor pd : pds) {
if (pd.getWriteMethod() != null) {
this.mappedFields.put(lowerCaseName(pd.getName()), pd);
this.mappedProperties.add(pd.getName());
}
}
}
/**
* 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
*/
@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);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
Set populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null);
final MappingHandler mappingHandler = this.jdbcEngineConfig.getMappingHandler();
for (int index = 1; index <= columnCount; index++) {
String column = JdbcUtils.lookupColumnName(rsmd, index);
String field = column.replaceAll(" ", "");
field = lowerCaseName(mappingHandler.getField(this.mappedClass, field));
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()) + "]");
}
if (this.jdbcEngineConfig.getJdbcTypeConverters() != null) {
JdbcTypeConverterComposite jdbcTypeConverterComposite = new JdbcTypeConverterComposite(this.jdbcEngineConfig.getJdbcTypeConverters());
if (jdbcTypeConverterComposite.support(this.dialect)) {
value = jdbcTypeConverterComposite.db2JavaType(this.dialect, pd.getPropertyType(), value);
}
}
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;
}
/**
* 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 the type parameter
* @param dialect the dialect
* @param jdbcEngineConfig the jdbc engine config
* @param mappedClass the class that each row should be mapped to
* @return the jdbc row mapper
*/
public static JdbcRowMapper newInstance(String dialect, JdbcEngineConfig jdbcEngineConfig, Class mappedClass) {
return new JdbcRowMapper<>(dialect, jdbcEngineConfig, mappedClass);
}
}