All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.java.ao.EntityProxy Maven / Gradle / Ivy

/*
 * Copyright 2007 Daniel Spiewak
 * 
 * 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 net.java.ao;

import net.java.ao.cache.CacheLayer;
import net.java.ao.schema.FieldNameConverter;
import net.java.ao.schema.NotNull;
import net.java.ao.schema.TableNameConverter;
import net.java.ao.sql.SqlUtils;
import net.java.ao.types.TypeInfo;
import net.java.ao.types.TypeManager;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;

import net.java.ao.types.LogicalType;

import static net.java.ao.Common.*;
import static net.java.ao.sql.SqlUtils.closeQuietly;

/**
 * @author Daniel Spiewak
 */
public class EntityProxy, K> implements InvocationHandler
{
    static boolean ignorePreload = false;	// hack for testing
	
	private final K key;
	private final Method pkAccessor;
	private final String pkFieldName;
	private final Class type;

	private final EntityManager manager;
	
	private CacheLayer layer;
	
	private Map locks;
	private final ReadWriteLock locksLock = new ReentrantReadWriteLock();
	
	private ImplementationWrapper implementation;
	private List listeners;

	public EntityProxy(EntityManager manager, Class type, K key) {
		this.key = key;
		this.type = type;
		this.manager = manager;
		
		pkAccessor = Common.getPrimaryKeyAccessor(type);
        pkFieldName = Common.getPrimaryKeyField(type, getFieldNameConverter());
		
		locks = new HashMap();

		listeners = new LinkedList();
	}

    private FieldNameConverter getFieldNameConverter()
    {
        return this.manager.getNameConverters().getFieldNameConverter();
    }

    @SuppressWarnings("unchecked")
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().equals("getEntityProxy")) {
            return this;
        }

		if (method.getName().equals("getEntityType")) {
			return type;
		}

		if (implementation == null) {
			implementation = new ImplementationWrapper();
			implementation.init((T) proxy);
		}

		MethodImplWrapper methodImpl = implementation.getMethod(method.getName(), method.getParameterTypes());
		if (methodImpl != null) {
			final Class declaringClass = methodImpl.getMethod().getDeclaringClass();
			if (!Object.class.equals(declaringClass)) {
                // We don't want to return get the class Class using Class.forName as this doesn't play well
                // with multiple ClassLoaders (if the AO library has a separate class loader to the AO client)
                // Instead just compare the classNames
				final String callingClassName = Common.getCallingClassName(1);
				if (callingClassName == null || !callingClassName.equals(declaringClass.getName())) {
					return methodImpl.getMethod().invoke(methodImpl.getInstance(), args);
				}
			}
		}

		if (method.getName().equals(pkAccessor.getName())) {
			return getKey();
		} else if (method.getName().equals("save")) {
			save((RawEntity) proxy);

			return Void.TYPE;
		} else if (method.getName().equals("getEntityManager")) {
            return manager;
        } else if (method.getName().equals("addPropertyChangeListener")) {
			addPropertyChangeListener((PropertyChangeListener) args[0]);
			return null;
		} else if (method.getName().equals("removePropertyChangeListener")) {
			removePropertyChangeListener((PropertyChangeListener) args[0]);
			return null;
		} else if (method.getName().equals("hashCode")) {
			return hashCodeImpl();
		} else if (method.getName().equals("equals")) {
			return equalsImpl((RawEntity) proxy, args[0]);
		} else if (method.getName().equals("toString")) {
			return toStringImpl();
		} else if (method.getName().equals("init")) {
			return null;
		}
		
		checkConstraints(method, args);

        String tableName = getTableNameConverter().getName(type);

		Class attributeType = Common.getAttributeTypeFromMethod(method);
		String polyFieldName = null;
		
		if (attributeType != null) {
            polyFieldName = (attributeType.getAnnotation(Polymorphic.class) == null ? null : 
				getFieldNameConverter().getPolyTypeName(method));
		}
		
		Mutator mutatorAnnotation = method.getAnnotation(Mutator.class);
		Accessor accessorAnnotation = method.getAnnotation(Accessor.class);
		OneToOne oneToOneAnnotation = method.getAnnotation(OneToOne.class);
		OneToMany oneToManyAnnotation = method.getAnnotation(OneToMany.class);
		ManyToMany manyToManyAnnotation = method.getAnnotation(ManyToMany.class);

        AnnotationDelegate annotations = Common.getAnnotationDelegate(getFieldNameConverter(), method);
		
		Transient transientAnnotation = annotations.getAnnotation(Transient.class);

		// check annotations first, they trump all
        if (oneToOneAnnotation != null && Common.interfaceInheritsFrom(method.getReturnType(), RawEntity.class)) {
			Class> type = (Class>) method.getReturnType();

            Object[] back = retrieveRelations((RawEntity) proxy, new String[0], 
					new String[] { Common.getPrimaryKeyField(type, getFieldNameConverter()) }, 
					(Class) type, Common.where(oneToOneAnnotation, getFieldNameConverter()),
					Common.getPolymorphicFieldNames(getFieldNameConverter(), type, this.type));
			
			return back.length == 0 ? null : back[0];
		} else if (oneToManyAnnotation != null && method.getReturnType().isArray() 
				&& Common.interfaceInheritsFrom(method.getReturnType().getComponentType(), RawEntity.class)) {
			Class> type = (Class>) method.getReturnType().getComponentType();

            return retrieveRelations((RawEntity) proxy, new String[0], 
					new String[] { Common.getPrimaryKeyField(type, getFieldNameConverter()) }, 
					(Class) type, where(oneToManyAnnotation, getFieldNameConverter()),
					Common.getPolymorphicFieldNames(getFieldNameConverter(), type, this.type));
		} else if (manyToManyAnnotation != null && method.getReturnType().isArray() 
				&& Common.interfaceInheritsFrom(method.getReturnType().getComponentType(), RawEntity.class)) {
			Class> throughType = manyToManyAnnotation.value();
			Class> type = (Class>) method.getReturnType().getComponentType();

            return retrieveRelations((RawEntity) proxy, null, 
					Common.getMappingFields(getFieldNameConverter(),
							throughType, type), throughType, (Class) type, 
							Common.where(manyToManyAnnotation, getFieldNameConverter()),
							Common.getPolymorphicFieldNames(getFieldNameConverter(), throughType, this.type),
							Common.getPolymorphicFieldNames(getFieldNameConverter(), throughType, type));
		} else if (Common.isAccessor(method)) {
            return invokeGetter((RawEntity) proxy, getKey(), tableName, getFieldNameConverter().getName(method),
                    polyFieldName, method.getReturnType(), transientAnnotation == null);
		} else if (Common.isMutator(method)) {
            invokeSetter((T) proxy, getFieldNameConverter().getName(method), args[0], polyFieldName);

			return Void.TYPE;
		}

		throw new RuntimeException("Cannot handle method with signature: " + method.toString());
	}

    private TableNameConverter getTableNameConverter()
    {
        return manager.getNameConverters().getTableNameConverter();
    }

    public K getKey() {
		return key;
	}

	@SuppressWarnings("unchecked")
	public void save(RawEntity entity) throws SQLException {
		CacheLayer cacheLayer = getCacheLayer(entity);
		String[] dirtyFields = cacheLayer.getDirtyFields();
		
		if (dirtyFields.length == 0) {
			return;
		}

        String table = getTableNameConverter().getName(type);
        final DatabaseProvider provider = this.manager.getProvider();
        final TypeManager typeManager = provider.getTypeManager();
        Connection conn = null;
        PreparedStatement stmt = null;
        try
        {
            conn = provider.getConnection();
			StringBuilder sql = new StringBuilder("UPDATE " + provider.withSchema(table) + " SET ");

			for (String field : dirtyFields) {
				sql.append(provider.processID(field));

				if (cacheLayer.contains(field)) {
					sql.append(" = ?,");
				} else {
					sql.append(" = NULL,");
				}
			}
			
			if (sql.charAt(sql.length() - 1) == ',') {
				sql.setLength(sql.length() - 1);
			}

			sql.append(" WHERE ").append(provider.processID(pkFieldName)).append(" = ?");

			stmt = provider.preparedStatement(conn, sql);

			List events = new LinkedList();
			int index = 1;
			for (String field : dirtyFields) {
				if (!cacheLayer.contains(field)) {
					continue;
				}
				
				Object value = cacheLayer.get(field);
				events.add(new PropertyChangeEvent(entity, field, null, value));
				
				if (value == null) {
                    this.manager.getProvider().putNull(stmt, index++);
				} else {
					Class javaType = value.getClass();

					if (value instanceof RawEntity) {
						javaType = ((RawEntity) value).getEntityType();
					}

					TypeInfo dbType = typeManager.getType(javaType);
                    dbType.getLogicalType().validate(value);
                    dbType.getLogicalType().putToDatabase(this.manager, stmt, index++, value, dbType.getJdbcWriteType());
					
					if (!dbType.getLogicalType().shouldCache(javaType)) {
						cacheLayer.remove(field);
					}
				}
			}
			TypeInfo pkType = Common.getPrimaryKeyType(provider.getTypeManager(), type);
            pkType.getLogicalType().putToDatabase(this.manager, stmt, index++, key, pkType.getJdbcWriteType());

            this.manager.getRelationsCache().remove(cacheLayer.getToFlush());
			cacheLayer.clearFlush();

            this.manager.getRelationsCache().remove(entity, dirtyFields);

			stmt.executeUpdate();

			for (PropertyChangeListener l : listeners) {
				for (PropertyChangeEvent evt : events) {
					l.propertyChange(evt);
				}
			}
			cacheLayer.clearDirty();
        }
        finally
        {
            closeQuietly(stmt);
            closeQuietly(conn);
        }
	}

	public void addPropertyChangeListener(PropertyChangeListener listener) {
		listeners.add(listener);
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		listeners.remove(listener);
	}

	public int hashCodeImpl() {
		return (key.hashCode() + type.hashCode()) % (2 << 15);
	}

	public boolean equalsImpl(RawEntity proxy, Object obj) {
		if (proxy == obj) {
			return true;
		}

		if (obj instanceof RawEntity) {
			RawEntity entity = (RawEntity) obj;

            String ourTableName = getTableNameConverter().getName(proxy.getEntityType());
            String theirTableName = getTableNameConverter().getName(entity.getEntityType());

			return Common.getPrimaryKeyValue(entity).equals(key) && theirTableName.equals(ourTableName);
		}

		return false;
	}

	public String toStringImpl() {
        return getTableNameConverter().getName(type) + " {" + pkFieldName + " = " + key.toString() + "}";
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}

		if (obj instanceof EntityProxy) {
			EntityProxy proxy = (EntityProxy) obj;

			if (proxy.type.equals(type) && proxy.key.equals(key)) {
				return true;
			}
		}

		return false;
	}

	@Override
	public int hashCode() {
		return hashCodeImpl();
	}

	CacheLayer getCacheLayer(RawEntity entity) {
		// not atomic, but throughput is more important in this case
		if (layer == null) {
			layer = manager.getCache().createCacheLayer(entity);
		}
		
		return layer;
	}

	Class getType() {
		return type;
	}

	// any dirty fields are kept in the cache, since they have yet to be saved
	void flushCache(RawEntity entity) {
		getCacheLayer(entity).clear();
	}

    private ReadWriteLock getLock(String field) {
		locksLock.writeLock().lock();
		try {
			if (locks.containsKey(field)) {
				return locks.get(field);
			}
			
			ReentrantReadWriteLock back = new ReentrantReadWriteLock();
			locks.put(field, back);
			
			return back;
		} finally {
			locksLock.writeLock().unlock();
		}
	}

	private  V invokeGetter(RawEntity entity, K key, String table, String name, String polyName, Class type, boolean shouldCache) throws Throwable {
		V back = null;
		CacheLayer cacheLayer = getCacheLayer(entity);
		
		shouldCache = shouldCache && getTypeManager().getType(type).getLogicalType().shouldCache(type);
		
		getLock(name).writeLock().lock();
		try {
			if (!shouldCache && cacheLayer.dirtyContains(name)) {
				return handleNullReturn(null, type);
			} else if (shouldCache && cacheLayer.contains(name)) {
				Object value = cacheLayer.get(name);
	
				if (instanceOf(value, type)) {
					return handleNullReturn((V) value, type);
                } else if (isBigDecimal(value, type)) { // Oracle for example returns BigDecimal when we expect doubles
                    return (V) handleBigDecimal(value, type);
				} else if (Common.interfaceInheritsFrom(type, RawEntity.class) 
						&& instanceOf(value, Common.getPrimaryKeyClassType((Class>) type))) {
                    value = manager.peer((Class>) type, value);
	
					cacheLayer.put(name, value);
					return handleNullReturn((V) value, type);
				} else {
					cacheLayer.remove(name); // invalid cached value
				}
			}

            final DatabaseProvider provider = manager.getProvider();
            Connection conn = null;
            PreparedStatement stmt = null;
            ResultSet res = null;
            try {
                conn = provider.getConnection();
				StringBuilder sql = new StringBuilder("SELECT ");
				
				sql.append(provider.processID(name));
				if (polyName != null) {
					sql.append(',').append(provider.processID(polyName));
				}
	
				sql.append(" FROM ").append(provider.withSchema(table)).append(" WHERE ");
				sql.append(provider.processID(pkFieldName)).append(" = ?");

				stmt = provider.preparedStatement(conn, sql);
				TypeInfo pkType = Common.getPrimaryKeyType(provider.getTypeManager(), this.type);
                pkType.getLogicalType().putToDatabase(manager, stmt, 1, key, pkType.getJdbcWriteType());
	
				res = stmt.executeQuery();
				if (res.next()) {
					back = convertValue(res, provider.shorten(name), provider.shorten(polyName), type);
				}
			} finally {
                closeQuietly(res, stmt, conn);
            }
	
			if (shouldCache) {
				cacheLayer.put(name, back);
			}
	
			return handleNullReturn(back, type);
		} finally {
			getLock(name).writeLock().unlock();
		}
	}

	private  V handleNullReturn(V back, Class type) {
		if (back != null) {
			return back;
		}
		
		if (type.isPrimitive()) {
			if (type.equals(boolean.class)) {
				return (V) new Boolean(false);
			} else if (type.equals(char.class)) {
				return (V) new Character(' ');
			} else if (type.equals(int.class)) {
				return (V) new Integer(0);
			} else if (type.equals(short.class)) {
				return (V) new Short("0");
			} else if (type.equals(long.class)) {
				return (V) new Long("0");
			} else if (type.equals(float.class)) {
				return (V) new Float("0");
			} else if (type.equals(double.class)) {
				return (V) new Double("0");
			} else if (type.equals(byte.class)) {
				return (V) new Byte("0");
			}
		}

		return null;
	}

	private void invokeSetter(T entity, String name, Object value, String polyName) throws Throwable {
		CacheLayer cacheLayer = getCacheLayer(entity);
		
		getLock(name).writeLock().lock();
		try {
			if (value instanceof RawEntity) {
				cacheLayer.markToFlush(((RawEntity) value).getEntityType());
				cacheLayer.markToFlush(entity.getEntityType());
			}

			cacheLayer.markDirty(name);
			cacheLayer.put(name, value);
	
			if (polyName != null) {
				String strValue = null;
	
				if (value != null) {
                    strValue = manager.getPolymorphicTypeMapper().convert(((RawEntity) value).getEntityType());
				}

				cacheLayer.markDirty(polyName);
				cacheLayer.put(polyName, strValue);
			}
		} finally {
			getLock(name).writeLock().unlock();
		}
	}

	private > V[] retrieveRelations(RawEntity entity, String[] inMapFields, 
			String[] outMapFields, Class type, String where, String[] thisPolyNames) throws SQLException {
		return retrieveRelations(entity, inMapFields, outMapFields, type, type, where, thisPolyNames, null);
	}

    private > V[] retrieveRelations(RawEntity entity,
                                                           String[] inMapFields,
                                                           String[] outMapFields,
                                                           Class> type,
                                                           Class finalType,
                                                           String where,
                                                           String[] thisPolyNames,
                                                           String[] thatPolyNames) throws SQLException
    {
		if (inMapFields == null || inMapFields.length == 0) {
            inMapFields = Common.getMappingFields(getFieldNameConverter(), type, this.type);
		}
        final String[] fields = getFields(Common.getPrimaryKeyField(finalType, getFieldNameConverter()),
				inMapFields, outMapFields, where);

        V[] cached = manager.getRelationsCache().get(entity, finalType, type, fields);
		if (cached != null) {
			return cached;
		}
		
		List back = new ArrayList();
		List> throughValues = new ArrayList>();
		List resPolyNames = new ArrayList(thatPolyNames == null ? 0 : thatPolyNames.length);

        String table = getTableNameConverter().getName(type);
		boolean oneToMany = type.equals(finalType);
		final Preload preloadAnnotation = finalType.getAnnotation(Preload.class);

        final DatabaseProvider provider = manager.getProvider();
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet res = null;
		try {
            conn = provider.getConnection();
			StringBuilder sql = new StringBuilder();
			String returnField;
			String throughField = null;
			int numParams = 0;
			
			Set selectFields = new LinkedHashSet();
			
			if (oneToMany && inMapFields.length == 1 && outMapFields.length == 1 
					&& preloadAnnotation != null && !ignorePreload) {
				sql.append("SELECT ");		// one-to-many preload
				
				selectFields.add(outMapFields[0]);
				selectFields.addAll(preloadValue(preloadAnnotation, getFieldNameConverter()));
				
				if (selectFields.contains(Preload.ALL)) {
					sql.append(Preload.ALL);
				} else {
					for (String field : selectFields) {
						sql.append(provider.processID(field)).append(',');
					}
					sql.setLength(sql.length() - 1);
				}
				
				sql.append(" FROM ").append(provider.withSchema(table));
				
				sql.append(" WHERE ").append(provider.processID(inMapFields[0])).append(" = ?");
				
				if (!where.trim().equals("")) {
                    sql.append(" AND (").append(manager.getProvider().processWhereClause(where)).append(")");
				}
				
				if (thisPolyNames != null) {
					for (String name : thisPolyNames) {
						sql.append(" AND ").append(provider.processID(name)).append(" = ?");
					}
				}
				
				numParams++;
				returnField = outMapFields[0];
			} else if (!oneToMany && inMapFields.length == 1 && outMapFields.length == 1
					&& preloadAnnotation != null && !ignorePreload) {
                String finalTable = getTableNameConverter().getName(finalType);		// many-to-many preload

                returnField = manager.getProvider().shorten(finalTable + "__aointernal__id");
                throughField = manager.getProvider().shorten(table + "__aointernal__id");

				sql.append("SELECT ");

                String finalPKField = Common.getPrimaryKeyField(finalType, getFieldNameConverter());

				selectFields.add(finalPKField);
				selectFields.addAll(preloadValue(preloadAnnotation, getFieldNameConverter()));

                if (selectFields.contains(Preload.ALL))
                {
                    selectFields.remove(Preload.ALL);
                    selectFields.addAll(Common.getValueFieldsNames(finalType, getFieldNameConverter()));
                }

                sql.append(provider.withSchema(finalTable)).append('.').append(provider.processID(finalPKField));
                sql.append(" AS ").append(provider.quote(returnField)).append(',');

                selectFields.remove(finalPKField);

                sql.append(provider.withSchema(table)).append('.').append(provider.processID(Common.getPrimaryKeyField(type, getFieldNameConverter())));
				sql.append(" AS ").append(provider.quote(throughField)).append(',');

				for (String field : selectFields) {
					sql.append(provider.withSchema(finalTable)).append('.').append(provider.processID(field)).append(',');
				}
				sql.setLength(sql.length() - 1);

				if (thatPolyNames != null) {
					for (String name : thatPolyNames) {
						String toAppend = table + '.' + name;

						resPolyNames.add(toAppend);
						sql.append(',').append(provider.processID(toAppend));
					}
				}

				sql.append(" FROM ").append(provider.withSchema(table)).append(" INNER JOIN ");
				sql.append(provider.withSchema(finalTable)).append(" ON ");
				sql.append(provider.withSchema(table)).append('.').append(provider.processID(outMapFields[0]));
				sql.append(" = ").append(provider.withSchema(finalTable)).append('.').append(provider.processID(finalPKField));

				sql.append(" WHERE ").append(provider.withSchema(table)).append('.').append(
						provider.processID(inMapFields[0])).append(" = ?");

				if (!where.trim().equals("")) {
                    sql.append(" AND (").append(manager.getProvider().processWhereClause(where)).append(")");
				}

				if (thisPolyNames != null) {
					for (String name : thisPolyNames) {
						sql.append(" AND ").append(provider.processID(name)).append(" = ?");
					}
				}

				numParams++;
			} else if (inMapFields.length == 1 && outMapFields.length == 1) {	// 99% case (1-* & *-*)
				sql.append("SELECT ").append(provider.processID(outMapFields[0]));
				selectFields.add(outMapFields[0]);

				if (!oneToMany) {
                    throughField = Common.getPrimaryKeyField(type, getFieldNameConverter());

					sql.append(',').append(provider.processID(throughField));
					selectFields.add(throughField);
				}

				if (thatPolyNames != null) {
					for (String name : thatPolyNames) {
						resPolyNames.add(name);
						sql.append(',').append(provider.processID(name));
						selectFields.add(name);
					}
				}

				sql.append(" FROM ").append(provider.withSchema(table));
				sql.append(" WHERE ").append(provider.processID(inMapFields[0])).append(" = ?");

				if (!where.trim().equals("")) {
                    sql.append(" AND (").append(manager.getProvider().processWhereClause(where)).append(")");
				}

				if (thisPolyNames != null) {
					for (String name : thisPolyNames) {
						sql.append(" AND ").append(provider.processID(name)).append(" = ?");
					}
				}

				numParams++;
				returnField = outMapFields[0];
			} else {
				sql.append("SELECT DISTINCT a.outMap AS outMap");
				selectFields.add("outMap");

				if (thatPolyNames != null) {
					for (String name : thatPolyNames) {
						resPolyNames.add(name);
						sql.append(',').append("a.").append(provider.processID(name)).append(" AS ").append(
								provider.processID(name));
						selectFields.add(name);
					}
				}

				sql.append(" FROM (");
				returnField = "outMap";

				for (String outMap : outMapFields) {
					for (String inMap : inMapFields) {
						sql.append("SELECT ");
						sql.append(provider.processID(outMap));
						sql.append(" AS outMap,");
						sql.append(provider.processID(inMap));
						sql.append(" AS inMap");

						if (thatPolyNames != null) {
							for (String name : thatPolyNames) {
								sql.append(',').append(provider.processID(name));
							}
						}

						if (thisPolyNames != null) {
							for (String name : thisPolyNames) {
								sql.append(',').append(provider.processID(name));
							}
						}

						sql.append(" FROM ").append(provider.withSchema(table));
						sql.append(" WHERE ");
						sql.append(provider.processID(inMap)).append(" = ?");

						if (!where.trim().equals("")) {
                            sql.append(" AND (").append(manager.getProvider().processWhereClause(where)).append(")");
						}

						sql.append(" UNION ");

						numParams++;
					}
				}

				sql.setLength(sql.length() - " UNION ".length());
				sql.append(") a");

				if (thatPolyNames != null) {
					if (thatPolyNames.length > 0) {
						sql.append(" WHERE (");
					}

					for (String name : thatPolyNames) {
						sql.append("a.").append(provider.processID(name)).append(" = ?").append(" OR ");
					}

					if (thatPolyNames.length > 0) {
						sql.setLength(sql.length() - " OR ".length());
						sql.append(')');
					}
				}

				if (thisPolyNames != null) {
					if (thisPolyNames.length > 0) {
						if (thatPolyNames == null) {
							sql.append(" WHERE (");
						} else {
							sql.append(" AND (");
						}
					}

					for (String name : thisPolyNames) {
						sql.append("a.").append(provider.processID(name)).append(" = ?").append(" OR ");
					}

					if (thisPolyNames.length > 0) {
						sql.setLength(sql.length() - " OR ".length());
						sql.append(')');
					}
				}
			}

			stmt = provider.preparedStatement(conn, sql);

            TypeInfo dbType =  getTypeManager().getType(getClass(key));
			int index = 0;
			for (; index < numParams; index++) {
                dbType.getLogicalType().putToDatabase(manager, stmt, index + 1, key, dbType.getJdbcWriteType());
			}
			
			int newLength = numParams + (thisPolyNames == null ? 0 : thisPolyNames.length);
            String typeValue = manager.getPolymorphicTypeMapper().convert(this.type);
			for (; index < newLength; index++) {
				stmt.setString(index + 1, typeValue);
			}

			dbType = Common.getPrimaryKeyType(provider.getTypeManager(), finalType);
			final TypeInfo throughDBType = Common.getPrimaryKeyType(provider.getTypeManager(), (Class>) type);
			
			res = stmt.executeQuery();
			while (res.next()) {
                K returnValue = dbType.getLogicalType().pullFromDatabase(manager, res, (Class) type, returnField);
				Class backType = finalType;
				
				for (String polyName : resPolyNames) {
					if ((typeValue = res.getString(polyName)) != null) {
                        backType = (Class) manager.getPolymorphicTypeMapper().invert(finalType, typeValue);
						break;
					}
				}
				
				if (backType.equals(this.type) && returnValue.equals(key)) {
					continue;
				}
				
				if (throughField != null) {
				    LogicalType logicalType = throughDBType.getLogicalType();
                    throughValues.add(manager.peer((Class>) type,
                            logicalType.pullFromDatabase(manager, res, type, throughField)));
				}

                V returnValueEntity = manager.peer(backType, returnValue);
                CacheLayer returnLayer = manager.getProxyForEntity(returnValueEntity).getCacheLayer(returnValueEntity);

                if (selectFields.contains(Preload.ALL))
                {
                    selectFields.remove(Preload.ALL);
                    selectFields.addAll(Common.getValueFieldsNames(finalType, getFieldNameConverter()));
                }
                for (String field : selectFields) {
                    if (!resPolyNames.contains(field)) {
						returnLayer.put(field, res.getObject(field));
					}
				}
				
				back.add(returnValueEntity);
			}
		} finally {
            closeQuietly(res, stmt, conn);
        }
		
		cached = back.toArray((V[]) Array.newInstance(finalType, back.size()));

        manager.getRelationsCache().put(entity, 
				(throughValues.size() > 0 ? throughValues.toArray(new RawEntity[throughValues.size()]) : cached), 
				type, cached, finalType, fields);
		
		return cached;
	}

    private TypeManager getTypeManager()
    {
        return manager.getProvider().getTypeManager();
    }

    /**
     * Gets the generic class of the given type
     * @param object the type for which to get the class
     * @return the generic class
     */
    @SuppressWarnings("unchecked")
    private static  Class getClass(O object)
    {
        return (Class) object.getClass();
    }

    private String[] getFields(String pkField, String[] inMapFields, String[] outMapFields, String where) {
		List back = new ArrayList();
		back.addAll(Arrays.asList(outMapFields));
		
		if (inMapFields != null && inMapFields.length > 0) {
			if (!inMapFields[0].trim().equalsIgnoreCase(pkField)) {
				back.addAll(Arrays.asList(inMapFields));
			}
		}
		
		Matcher matcher = SqlUtils.WHERE_CLAUSE.matcher(where);
		while (matcher.find()) {
			back.add(matcher.group(1));
		}
		
		return back.toArray(new String[back.size()]);
	}

    private  V convertValue(ResultSet res, String field, String polyName, Class type) throws SQLException
    {
        if (isNull(res, field))
        {
            return null;
        }
		
		if (polyName != null) {
			Class> entityType = (Class>) type;
            entityType = manager.getPolymorphicTypeMapper().invert(entityType, res.getString(polyName));
			
			type = (Class) entityType;		// avoiding Java cast oddities with generics
		}

        final TypeInfo databaseType = getTypeManager().getType(type);
		
		if (databaseType == null) {
			throw new RuntimeException("UnrecognizedType: " + type.toString());
		}

        return databaseType.getLogicalType().pullFromDatabase(this.manager, res, type, field);
	}

    private boolean isNull(ResultSet res, String field) throws SQLException
    {
        res.getObject(field);
        return res.wasNull();
    }

    private boolean instanceOf(Object value, Class type) {
		if (value == null) {
			return true;
		}
		
		if (type.isPrimitive()) {
			if (type.equals(boolean.class)) {
				return instanceOf(value, Boolean.class);
			} else if (type.equals(char.class)) {
				return instanceOf(value, Character.class);
			} else if (type.equals(byte.class)) {
				return instanceOf(value, Byte.class);
			} else if (type.equals(short.class)) {
				return instanceOf(value, Short.class);
			} else if (type.equals(int.class)) {
				return instanceOf(value, Integer.class);
			} else if (type.equals(long.class)) {
				return instanceOf(value, Long.class);
			} else if (type.equals(float.class)) {
				return instanceOf(value, Float.class);
			} else if (type.equals(double.class)) {
				return instanceOf(value, Double.class);
			}
		} else {
			return type.isInstance(value);
		}

		return false;
	}

    /**
     * Some DB (Oracle) return BigDecimal for about any number
     */
    private boolean isBigDecimal(Object value, Class type)
    {
        if (!(value instanceof BigDecimal))
        {
            return false;
        }
        return isInteger(type) || isLong(type) || isFloat(type) || isDouble(type);
    }

    private Object handleBigDecimal(Object value, Class type)
    {
        final BigDecimal bd = (BigDecimal) value;
        if (isInteger(type))
        {
            return bd.intValue();
        }
        else if (isLong(type))
        {
            return bd.longValue();
        }
        else if (isFloat(type))
        {
            return bd.floatValue();
        }
        else if (isDouble(type))
        {
            return bd.doubleValue();
        }
        else
        {
            throw new RuntimeException("Could not resolve actual type for object :" + value + ", expected type is " + type);
        }
    }

    private boolean isDouble(Class type)
    {
        return type.equals(double.class) || type.equals(Double.class);
    }

    private boolean isFloat(Class type)
    {
        return type.equals(float.class) || type.equals(Float.class);
    }

    private boolean isLong(Class type)
    {
        return type.equals(long.class) || type.equals(Long.class);
    }

    private boolean isInteger(Class type)
    {
        return type.equals(int.class) || type.equals(Integer.class);
    }

    private void checkConstraints(Method method, Object[] args) {
        AnnotationDelegate annotations = Common.getAnnotationDelegate(getFieldNameConverter(), method);
		
		NotNull notNullAnnotation = annotations.getAnnotation(NotNull.class);
		if (notNullAnnotation != null && args != null && args.length > 0) {
			if (args[0] == null) {
                String name = getFieldNameConverter().getName(method);
				throw new IllegalArgumentException("Field '" + name + "' does not accept null values");
			}
		}
	}
}