/**
   * Deletes the specified entities from the database. DELETE statements are called on the rows in
   * the corresponding tables and the entities are removed from the instance cache. The entity
   * instances themselves are not invalidated, but it doesn't even make sense to continue using the
   * instance without a row with which it is paired.
   *
   * <p>This method does attempt to group the DELETE statements on a per-type basis. Thus, if you
   * pass 5 instances of <code>EntityA</code> and two instances of <code>EntityB</code>, the
   * following SQL prepared statements will be invoked:
   *
   * <pre>DELETE FROM entityA WHERE id IN (?,?,?,?,?);
   * DELETE FROM entityB WHERE id IN (?,?);</pre>
   *
   * <p>Thus, this method scales very well for large numbers of entities grouped into types.
   * However, the execution time increases linearly for each entity of unique type.
   *
   * @param entities A varargs array of entities to delete. Method returns immediately if length ==
   *     0.
   */
  @SuppressWarnings("unchecked")
  public void delete(RawEntity<?>... entities) throws SQLException {
    if (entities.length == 0) {
      return;
    }

    Map<Class<? extends RawEntity<?>>, List<RawEntity<?>>> organizedEntities =
        new HashMap<Class<? extends RawEntity<?>>, List<RawEntity<?>>>();

    for (RawEntity<?> entity : entities) {
      verify(entity);
      Class<? extends RawEntity<?>> type = getProxyForEntity(entity).getType();

      if (!organizedEntities.containsKey(type)) {
        organizedEntities.put(type, new LinkedList<RawEntity<?>>());
      }
      organizedEntities.get(type).add(entity);
    }

    entityCacheLock.writeLock().lock();
    try {
      DatabaseProvider provider = getProvider();
      Connection conn = provider.getConnection();
      try {
        for (Class<? extends RawEntity<?>> type : organizedEntities.keySet()) {
          List<RawEntity<?>> entityList = organizedEntities.get(type);

          StringBuilder sql = new StringBuilder("DELETE FROM ");

          tableNameConverterLock.readLock().lock();
          try {
            sql.append(provider.processID(tableNameConverter.getName(type)));
          } finally {
            tableNameConverterLock.readLock().unlock();
          }

          sql.append(" WHERE ")
              .append(provider.processID(Common.getPrimaryKeyField(type, getFieldNameConverter())))
              .append(" IN (?");

          for (int i = 1; i < entityList.size(); i++) {
            sql.append(",?");
          }
          sql.append(')');

          Logger.getLogger("net.java.ao").log(Level.INFO, sql.toString());
          PreparedStatement stmt = conn.prepareStatement(sql.toString());

          int index = 1;
          for (RawEntity<?> entity : entityList) {
            TypeManager.getInstance()
                .getType((Class) entity.getEntityType())
                .putToDatabase(this, stmt, index++, entity);
          }

          relationsCache.remove(type);
          stmt.executeUpdate();
          stmt.close();
        }
      } finally {
        conn.close();
      }

      for (RawEntity<?> entity : entities) {
        entityCache.remove(new CacheKey(Common.getPrimaryKeyValue(entity), entity.getEntityType()));
      }

      proxyLock.writeLock().lock();
      try {
        for (RawEntity<?> entity : entities) {
          proxies.remove(entity);
        }
      } finally {
        proxyLock.writeLock().unlock();
      }
    } finally {
      entityCacheLock.writeLock().unlock();
    }
  }
  /**
   * Executes the specified SQL and extracts the given key field, wrapping each row into a instance
   * of the specified type. The SQL itself is executed as a {@link PreparedStatement} with the given
   * parameters.
   *
   * <p>Example:
   *
   * <pre>
   * manager.findWithSQL(Person.class, "personID", "SELECT personID FROM chairs WHERE position &lt; ? LIMIT ?", 10, 5);
   * </pre>
   *
   * <p>The SQL is not parsed or modified in any way by ActiveObjects. As such, it is possible to
   * execute database-specific queries using this method without realizing it. For example, the
   * above query will not run on MS SQL Server or Oracle, due to the lack of a LIMIT clause in their
   * SQL implementation. As such, be extremely careful about what SQL is executed using this method,
   * or else be conscious of the fact that you may be locking yourself to a specific DBMS.
   *
   * @param type The type of the entities to retrieve.
   * @param keyField The field value to use in the creation of the entities. This is usually the
   *     primary key field of the corresponding table.
   * @param sql The SQL statement to execute.
   * @param parameters A varargs array of parameters to be passed to the executed prepared
   *     statement. The length of this array <i>must</i> match the number of parameters (denoted by
   *     the '?' char) in the <code>criteria</code>.
   * @return An array of entities of the given type which match the specified query.
   */
  @SuppressWarnings("unchecked")
  public <T extends RawEntity<K>, K> T[] findWithSQL(
      Class<T> type, String keyField, String sql, Object... parameters) throws SQLException {
    List<T> back = new ArrayList<T>();

    Connection conn = getProvider().getConnection();
    try {
      Logger.getLogger("net.java.ao").log(Level.INFO, sql);
      PreparedStatement stmt = conn.prepareStatement(sql);

      TypeManager manager = TypeManager.getInstance();
      for (int i = 0; i < parameters.length; i++) {
        Class javaType = parameters[i].getClass();

        if (parameters[i] instanceof RawEntity) {
          javaType = ((RawEntity<?>) parameters[i]).getEntityType();
        }

        manager.getType(javaType).putToDatabase(this, stmt, i + 1, parameters[i]);
      }

      ResultSet res = stmt.executeQuery();
      while (res.next()) {
        back.add(
            peer(
                type,
                Common.getPrimaryKeyType(type)
                    .pullFromDatabase(this, res, (Class<? extends K>) type, keyField)));
      }
      res.close();
      stmt.close();
    } finally {
      conn.close();
    }

    return back.toArray((T[]) Array.newInstance(type, back.size()));
  }