jodd.db.oom.mapper.DefaultResultSetMapper Maven / Gradle / Ivy
Show all versions of jodd-db Show documentation
// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
package jodd.db.oom.mapper;
import jodd.bean.BeanUtil;
import jodd.db.DbOom;
import jodd.db.oom.ColumnData;
import jodd.db.oom.DbEntityColumnDescriptor;
import jodd.db.oom.DbEntityDescriptor;
import jodd.db.oom.DbEntityManager;
import jodd.db.oom.DbOomException;
import jodd.db.oom.DbOomQuery;
import jodd.db.type.SqlType;
import jodd.db.type.SqlTypeManager;
import jodd.typeconverter.TypeConverterManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Maps all columns of database result set (RS) row to objects.
* It does it in two steps: preparation (reading table and column names)
* and parsing (parsing one result set row to resulting objects).
*
* Preparation
* Default mapper reads RS column and table names from RS meta-data and external maps, if provided.
* Since column name is always available in RS meta-data, it may be used to hold table name information.
* Column names may contain table code separator that
* divides column name to table reference and column name. Here, table reference may be either table name or
* table alias. When it is table alias, external alias-to-name map must be provided.
* Hence, this defines the table name, and there is no need to read it from RS meta-data.
*
* When column name doesn't contain a separator, it may be either an actual column name, or a column code.
* For column codes, both table and column name is lookup-ed from external map. If column name is an actual column name,
* table information is read from the RS meta data. Unfortunately, some DBs (such Oracle) doesn't implements
* this simple JDBC feature. Therefore, it must be expected that column table name is not available.
*
* Table name is also not available for columns which are not directly table columns:
* e.g. some calculations, counts etc.
*
* Parsing
* Parser takes types array and tries to populate their instances in best possible way. It assumes that provided
* types list matches selected columns. That is very important, and yet very easy and natural to follow.
* So, parser will try to inject columns value into the one result instance. Now, there are two types of instances:
* simple types (numbers and strings) and entities (pojo objects). Simple types are always mapped to
* one and only one column. Entities will be mapped to all possible columns that can be matched starting from
* current column. So, simple types are not column-hungry, entity types are column-hungry:)
*
*
* A column can be injected in one entities property only once. If one column is already mapped to current result,
* RS mapper will assume that current result is finished with mapping and will proceed to the next one.
* Similarly, if property name is not found for a column, RS mapper will proceed to the next result.
* Therefore, entity types are column precise and hungry;) - all listed columns must be mapped somewhere.
*
*
* Results that are not used during parsing will be set to null
.
*
*/
public class DefaultResultSetMapper extends BaseResultSetMapper {
protected final DbOomQuery dbOomQuery;
protected final boolean cacheEntities;
protected final int totalColumns; // total number of columns
protected final String[] columnNames; // list of all column names
protected final int[] columnDbSqlTypes; // list of all column db types
protected final String[] tableNames; // list of table names for each column, table name may be null
private final Set resultColumns; // internal columns per entity cache
private final DbEntityManager dbEntityManager;
// ---------------------------------------------------------------- ctor
/**
* Reads ResultSet
meta-data for column and table names.
* @param resultSet JDBC result set
* @param columnAliases alias names for columns, if exist
* @param cacheEntities flag if entities should be cached
* @param dbOomQuery query that created this mapper.
*/
public DefaultResultSetMapper(
final DbOom dbOom,
final ResultSet resultSet,
final Map columnAliases,
final boolean cacheEntities,
final DbOomQuery dbOomQuery) {
super(resultSet);
this.dbEntityManager = dbOom.entityManager();
this.dbOomQuery = dbOomQuery;
this.cacheEntities = cacheEntities;
//this.resultColumns = new HashSet();
try {
ResultSetMetaData rsMetaData = resultSet.getMetaData();
if (rsMetaData == null) {
throw new DbOomException("No ResultSet meta-data");
}
totalColumns = rsMetaData.getColumnCount();
this.resultColumns = new HashSet<>(totalColumns);
columnNames = new String[totalColumns];
columnDbSqlTypes = new int[totalColumns];
tableNames = new String[totalColumns];
for (int i = 0; i < totalColumns; i++) {
String columnName = rsMetaData.getColumnLabel(i + 1);
if (columnName == null) {
columnName = rsMetaData.getColumnName(i + 1);
}
String tableName = null;
// resolve column and table name
int sepNdx = columnName.indexOf(dbOom.config().getColumnAliasSeparator());
if (sepNdx != -1) {
// column alias exist, result set is ignored and columnAliases contains table data
tableName = columnName.substring(0, sepNdx);
if (columnAliases != null) {
ColumnData columnData = columnAliases.get(tableName);
if (columnData != null) {
tableName = columnData.getTableName();
}
}
columnName = columnName.substring(sepNdx + 1);
} else {
// column alias does not exist, table name is read from columnAliases and result set (if available)
if (columnAliases != null) {
ColumnData columnData = columnAliases.get(columnName);
if (columnData != null) {
tableName = columnData.getTableName();
columnName = columnData.getColumnName();
}
}
if (tableName == null) {
try {
tableName = rsMetaData.getTableName(i + 1);
} catch (SQLException sex) {
// ignore
}
if ((tableName != null) && (tableName.length() == 0)) {
tableName = null;
}
}
}
columnName = columnName.trim();
if (columnName.length() == 0) {
columnName = null;
}
if (columnName != null) {
columnName = columnName.trim();
columnName = columnName.toUpperCase();
}
columnNames[i] = columnName;
if (tableName != null) {
tableName = tableName.trim();
tableName = tableName.toUpperCase();
}
tableNames[i] = tableName;
columnDbSqlTypes[i] = rsMetaData.getColumnType(i + 1);
}
} catch (SQLException sex) {
throw new DbOomException(dbOomQuery, "Reading ResultSet meta-data failed", sex);
}
}
// ---------------------------------------------------------------- delegates
/**
* {@inheritDoc}
*/
@Override
public Class[] resolveTables() {
List classes = new ArrayList<>(tableNames.length);
String lastTableName = null;
resultColumns.clear();
for (int i = 0; i < tableNames.length; i++) {
String tableName = tableNames[i];
String columnName = columnNames[i];
if (tableName == null) {
// maybe JDBC driver does not support it
throw new DbOomException(dbOomQuery, "Table name missing in meta-data");
}
if ((!tableName.equals(lastTableName)) || (resultColumns.contains(columnName))) {
resultColumns.clear();
lastTableName = tableName;
DbEntityDescriptor ded = dbEntityManager.lookupTableName(tableName);
if (ded == null) {
throw new DbOomException(dbOomQuery, "Table name not registered: " + tableName);
}
classes.add(ded.getType());
}
resultColumns.add(columnName);
}
return classes.toArray(new Class[0]);
}
// ---------------------------------------------------------------- cache
protected DbEntityDescriptor[] cachedDbEntityDescriptors;
protected Class[] cachedUsedTypes;
protected String[] cachedTypesTableNames;
protected String[][] cachedMappedNames;
/**
* Resolves {@link jodd.db.oom.DbEntityDescriptor} for all given types,
* so not to repeat every time.
*/
protected DbEntityDescriptor[] resolveDbEntityDescriptors(final Class[] types) {
if (cachedDbEntityDescriptors == null) {
DbEntityDescriptor[] descs = new DbEntityDescriptor[types.length];
for (int i = 0; i < types.length; i++) {
Class type = types[i];
if (type != null) {
descs[i] = dbEntityManager.lookupType(type);
}
}
cachedDbEntityDescriptors = descs;
}
return cachedDbEntityDescriptors;
}
/**
* Creates table names for all specified types.
* Since this is usually done once per result set, these names are cached.
* Type name will be null
for simple names, i.e. for all those
* types that returns null
when used by {@link DbEntityManager#lookupType(Class)}.
*/
protected String[] resolveTypesTableNames(final Class[] types) {
if (types != cachedUsedTypes) {
cachedTypesTableNames = createTypesTableNames(types);
cachedUsedTypes = types;
}
return cachedTypesTableNames;
}
/**
* Resolved mapped type names for each type.
*/
protected String[][] resolveMappedTypesTableNames(final Class[] types) {
if (cachedMappedNames == null) {
String[][] names = new String[types.length][];
for (int i = 0; i < types.length; i++) {
Class type = types[i];
if (type != null) {
DbEntityDescriptor ded = cachedDbEntityDescriptors[i];
if (ded != null) {
Class[] mappedTypes = ded.getMappedTypes();
if (mappedTypes != null) {
names[i] = createTypesTableNames(mappedTypes);
}
}
}
}
cachedMappedNames = names;
}
return cachedMappedNames;
}
/**
* Creates table names for given types.
*/
protected String[] createTypesTableNames(final Class[] types) {
String[] names = new String[types.length];
for (int i = 0; i < types.length; i++) {
if (types[i] == null) {
names[i] = null;
continue;
}
DbEntityDescriptor ded = dbEntityManager.lookupType(types[i]);
if (ded != null) {
String tableName = ded.getTableName();
tableName = tableName.toUpperCase();
names[i] = tableName;
}
}
return names;
}
protected int cachedColumnNdx;
protected Object cachedColumnValue;
// ---------------------------------------------------------------- parse object
/**
* Reads column value from result set. Since this method may be called more then once for
* the same column, it caches column values.
*/
@SuppressWarnings({"unchecked"})
protected Object readColumnValue(final int colNdx, final Class destinationType, final Class extends SqlType> sqlTypeClass, final int columnDbSqlType) {
if (colNdx != cachedColumnNdx) {
try {
SqlType sqlType;
if (sqlTypeClass != null) {
sqlType = SqlTypeManager.get().lookupSqlType(sqlTypeClass);
} else {
sqlType = SqlTypeManager.get().lookup(destinationType);
}
if (sqlType != null) {
cachedColumnValue = sqlType.readValue(resultSet, colNdx + 1, destinationType, columnDbSqlType);
} else {
cachedColumnValue = resultSet.getObject(colNdx + 1);
cachedColumnValue = TypeConverterManager.get().convertType(cachedColumnValue, destinationType);
}
} catch (SQLException sex) {
throw new DbOomException(dbOomQuery, "Invalid value for column #" + (colNdx + 1), sex);
}
cachedColumnNdx = colNdx;
}
return cachedColumnValue;
}
/**
* {@inheritDoc}
*/
@Override
public Object[] parseObjects(final Class... types) {
resultColumns.clear();
int totalTypes = types.length;
Object[] result = new Object[totalTypes];
boolean[] resultUsage = new boolean[totalTypes];
DbEntityDescriptor[] dbEntityDescriptors = resolveDbEntityDescriptors(types);
String[] typesTableNames = resolveTypesTableNames(types);
String[][] mappedNames = resolveMappedTypesTableNames(types);
int currentResult = 0;
cachedColumnNdx = -1;
int colNdx = 0;
while (colNdx < totalColumns) {
// no more types for mapping?
if (currentResult >= totalTypes) {
break;
}
// skip columns that doesn't map
Class currentType = types[currentResult];
if (currentType == null) {
colNdx++;
currentResult++;
resultColumns.clear();
continue;
}
String columnName = columnNames[colNdx];
int columnDbSqlType = columnDbSqlTypes[colNdx];
String tableName = tableNames[colNdx];
String resultTableName = typesTableNames[currentResult];
if (resultTableName == null) {
// match: simple type
result[currentResult] = readColumnValue(colNdx, currentType, null, columnDbSqlType);
resultUsage[currentResult] = true;
colNdx++;
currentResult++; resultColumns.clear();
continue;
}
// match table
boolean tableMatched = false;
if (tableName == null) {
tableMatched = true;
} else if (resultTableName.equals(tableName)) {
tableMatched = true;
} else {
String[] mapped = mappedNames[currentResult];
if (mapped != null) {
for (String m : mapped) {
if (m.equals(tableName)) {
tableMatched = true;
break;
}
}
}
}
if (tableMatched) {
if (!resultColumns.contains(columnName)) {
//DbEntityDescriptor ded = dbEntityManager.lookupType(currentType);
DbEntityDescriptor ded = dbEntityDescriptors[currentResult];
DbEntityColumnDescriptor dec = ded.findByColumnName(columnName);
String propertyName = (dec == null ? null : dec.getPropertyName());
// check if a property that matches column name exist
if (propertyName != null) {
// if current entity instance does not exist (i.e. we are at the first column
// of some entity), create the instance and store it
if (result[currentResult] == null) {
result[currentResult] = dbEntityManager.createEntityInstance(currentType);
}
/*
boolean success = value != null ?
BeanUtil.setDeclaredPropertySilent(result[currentResult], propertyName, value) :
BeanUtil.hasDeclaredProperty(result[currentResult], propertyName);
*/
Class type = BeanUtil.declared.getPropertyType(result[currentResult], propertyName);
if (type != null) {
// match: entity
dec.updateDbSqlType(columnDbSqlType); // updates column db sql type information for the entity!!!
Class extends SqlType> sqlTypeClass = dec.getSqlTypeClass();
Object value = readColumnValue(colNdx, type, sqlTypeClass, columnDbSqlType);
if (value != null) {
// inject column value into existing entity
BeanUtil.declared.setProperty(result[currentResult], propertyName, value);
resultUsage[currentResult] = true;
}
colNdx++;
resultColumns.add(columnName);
continue;
}
}
}
}
// go to next type, i.e. result
currentResult++;
resultColumns.clear();
}
resultColumns.clear();
for (int i = 0; i < resultUsage.length; i++) {
if (!resultUsage[i]) {
result[i] = null;
}
}
if (cacheEntities) {
cacheResultSetEntities(result);
}
return result;
}
// ---------------------------------------------------------------- cache
protected HashMap