private void updateBBoxCache() throws FeatureStoreException {

    // beware of concurrent transactions
    synchronized (fs) {
      Set<QName> recalcFTs = bboxTracker.getRecalcFeatureTypes();
      Map<QName, Envelope> ftNamesToIncreaseBBoxes = bboxTracker.getIncreaseBBoxes();

      // handle bbox increases
      for (Entry<QName, Envelope> ftNameToIncreaseBBox : ftNamesToIncreaseBBoxes.entrySet()) {
        QName ftName = ftNameToIncreaseBBox.getKey();
        Envelope bbox = null;
        if (fs.getBBoxCache().contains(ftName)) {
          bbox = ftNameToIncreaseBBox.getValue();
        }
        if (bbox != null) {
          Envelope oldBbox = fs.getBBoxCache().get(ftName);
          if (oldBbox != null) {
            bbox = oldBbox.merge(bbox);
          }
          fs.getBBoxCache().set(ftName, bbox);
        }
      }

      // TODO configuration switch for bbox recalculation strategy
      if (!recalcFTs.isEmpty()) {
        LOG.debug(
            "Full recalculation of feature type envelopes required. Delete 'bbox_cache.properties' if you need minimal envelopes.");
      }

      try {
        fs.getBBoxCache().persist();
      } catch (Throwable t) {
        LOG.error("Unable to persist bbox cache: " + t.getMessage());
      }
    }
  }
  @Override
  public int performDelete(IdFilter filter, Lock lock) throws FeatureStoreException {
    int deleted = 0;
    if (blobMapping != null) {
      deleted = performDeleteBlob(filter, lock);
    } else {
      deleted = performDeleteRelational(filter, lock);
    }

    // TODO improve this
    for (FeatureType ft : schema.getFeatureTypes(null, false, false)) {
      bboxTracker.delete(ft.getName());
    }

    return deleted;
  }
 @Override
 public List<String> performUpdate(
     QName ftName, List<ParsedPropertyReplacement> replacementProps, Filter filter, Lock lock)
     throws FeatureStoreException {
   LOG.debug(
       "Updating feature type '"
           + ftName
           + "', filter: "
           + filter
           + ", replacement properties: "
           + replacementProps.size());
   List<String> updatedFids = null;
   if (blobMapping != null) {
     updatedFids = performUpdateBlob(ftName, replacementProps, filter, lock);
   } else {
     updatedFids = performUpdateRelational(ftName, replacementProps, filter);
   }
   bboxTracker.update(ftName);
   return updatedFids;
 }
  @Override
  public List<String> performInsert(FeatureCollection fc, IDGenMode mode)
      throws FeatureStoreException {

    LOG.debug("performInsert()");

    Set<Geometry> geometries = new LinkedHashSet<Geometry>();
    Set<Feature> features = new LinkedHashSet<Feature>();
    Set<String> fids = new LinkedHashSet<String>();
    Set<String> gids = new LinkedHashSet<String>();
    for (Feature member : fc) {
      findFeaturesAndGeometries(member, geometries, features, fids, gids);
    }

    LOG.debug(features.size() + " features / " + geometries.size() + " geometries");

    for (FeatureInspector inspector : inspectors) {
      for (Feature f : features) {
        // TODO cope with inspectors that return a different instance
        inspector.inspect(f);
      }
    }

    long begin = System.currentTimeMillis();

    String fid = null;
    try {
      PreparedStatement blobInsertStmt = null;
      if (blobMapping != null) {
        switch (mode) {
          case GENERATE_NEW:
            {
              // TODO don't change incoming features / geometries
              for (Feature feature : features) {
                String newFid = "FEATURE_" + generateNewId();
                String oldFid = feature.getId();
                if (oldFid != null) {
                  fids.remove(oldFid);
                }
                fids.add(newFid);
                feature.setId(newFid);
              }
              for (Geometry geometry : geometries) {
                String newGid = "GEOMETRY_" + generateNewId();
                String oldGid = geometry.getId();
                if (oldGid != null) {
                  gids.remove(oldGid);
                }
                gids.add(newGid);
                geometry.setId(newGid);
              }
              break;
            }
          case REPLACE_DUPLICATE:
            {
              throw new FeatureStoreException("REPLACE_DUPLICATE is not available yet.");
            }
          case USE_EXISTING:
            {
              // TODO don't change incoming features / geometries
              for (Feature feature : features) {
                if (feature.getId() == null) {
                  String newFid = "FEATURE_" + generateNewId();
                  feature.setId(newFid);
                  fids.add(newFid);
                }
              }

              for (Geometry geometry : geometries) {
                if (geometry.getId() == null) {
                  String newGid = "GEOMETRY_" + generateNewId();
                  geometry.setId(newGid);
                  gids.add(newGid);
                }
              }
              break;
            }
        }
        StringBuilder sql = new StringBuilder("INSERT INTO ");
        sql.append(blobMapping.getTable());
        sql.append(" (");
        sql.append(blobMapping.getGMLIdColumn());
        sql.append(",");
        sql.append(blobMapping.getTypeColumn());
        sql.append(",");
        sql.append(blobMapping.getDataColumn());
        sql.append(",");
        sql.append(blobMapping.getBBoxColumn());
        sql.append(") VALUES(?,?,?,");
        sql.append(blobGeomConverter.getSetSnippet(null));
        sql.append(")");
        LOG.debug("Inserting: {}", sql);
        blobInsertStmt = conn.prepareStatement(sql.toString());
        for (Feature feature : features) {
          fid = feature.getId();
          if (blobInsertStmt != null) {
            insertFeatureBlob(blobInsertStmt, feature);
          }
          FeatureTypeMapping ftMapping = fs.getMapping(feature.getName());
          if (ftMapping != null) {
            throw new UnsupportedOperationException();
          }
          ICRS storageSrs = blobMapping.getCRS();
          bboxTracker.insert(feature, storageSrs);
        }
        if (blobInsertStmt != null) {
          blobInsertStmt.close();
        }
      } else {
        // pure relational mode
        List<FeatureRow> idAssignments = new ArrayList<FeatureRow>();
        InsertRowManager insertManager = new InsertRowManager(fs, conn, mode);
        for (Feature feature : features) {
          FeatureTypeMapping ftMapping = fs.getMapping(feature.getName());
          if (ftMapping == null) {
            throw new FeatureStoreException(
                "Cannot insert feature of type '"
                    + feature.getName()
                    + "'. No mapping defined and BLOB mode is off.");
          }
          idAssignments.add(insertManager.insertFeature(feature, ftMapping));
          Pair<TableName, GeometryMapping> mapping = ftMapping.getDefaultGeometryMapping();
          if (mapping != null) {
            ICRS storageSrs = mapping.second.getCRS();
            bboxTracker.insert(feature, storageSrs);
          }
        }
        if (insertManager.getDelayedRows() != 0) {
          String msg =
              "After insertion, "
                  + insertManager.getDelayedRows()
                  + " delayed rows left uninserted. Probably a cyclic key constraint blocks insertion.";
          throw new RuntimeException(msg);
        }
        // TODO why is this necessary?
        fids.clear();
        for (FeatureRow assignment : idAssignments) {
          fids.add(assignment.getNewId());
        }
      }
    } catch (Throwable t) {
      String msg = "Error inserting feature: " + t.getMessage();
      LOG.error(msg);
      LOG.trace("Stack trace:", t);
      throw new FeatureStoreException(msg, t);
    }

    long elapsed = System.currentTimeMillis() - begin;
    LOG.debug("Insertion of " + features.size() + " features: " + elapsed + " [ms]");
    return new ArrayList<String>(fids);
  }