org.apache.cayenne.dba.JdbcPkGenerator Maven / Gradle / Ivy
Show all versions of cayenne-client-nodeps
/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cayenne.dba;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Level;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.access.DataNode;
import org.apache.cayenne.access.OperationObserver;
import org.apache.cayenne.access.QueryLogger;
import org.apache.cayenne.access.ResultIterator;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbKeyGenerator;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.util.IDUtil;
/**
* Default primary key generator implementation. Uses a lookup table named
* "AUTO_PK_SUPPORT" to search and increment primary keys for tables.
*
* @author Andrei Adamchik
*/
public class JdbcPkGenerator implements PkGenerator {
public static final int DEFAULT_PK_CACHE_SIZE = 20;
/**
* @deprecated Since 1.2 unused.
*/
protected static final String NEXT_ID = "NEXT_ID";
/**
* @deprecated Since 1.2 unused.
*/
protected static final ObjAttribute[] objDesc = new ObjAttribute[] {
new ObjAttribute("nextId", Integer.class.getName(), null)
};
/**
* @deprecated Since 1.2 unused.
*/
protected static final DbAttribute[] resultDesc = new DbAttribute[] {
new DbAttribute(NEXT_ID, Types.INTEGER, null)
};
protected Map pkCache = new HashMap();
protected int pkCacheSize = DEFAULT_PK_CACHE_SIZE;
static {
objDesc[0].setDbAttributePath(NEXT_ID);
}
public void createAutoPk(DataNode node, List dbEntities) throws Exception {
// check if a table exists
// create AUTO_PK_SUPPORT table
if (!autoPkTableExists(node)) {
runUpdate(node, pkTableCreateString());
}
// delete any existing pk entries
runUpdate(node, pkDeleteString(dbEntities));
// insert all needed entries
Iterator it = dbEntities.iterator();
while (it.hasNext()) {
DbEntity ent = (DbEntity) it.next();
runUpdate(node, pkCreateString(ent.getName()));
}
}
public List createAutoPkStatements(List dbEntities) {
List list = new ArrayList();
list.add(pkTableCreateString());
list.add(pkDeleteString(dbEntities));
Iterator it = dbEntities.iterator();
while (it.hasNext()) {
DbEntity ent = (DbEntity) it.next();
list.add(pkCreateString(ent.getName()));
}
return list;
}
/**
* Drops table named "AUTO_PK_SUPPORT" if it exists in the database.
*/
public void dropAutoPk(DataNode node, List dbEntities) throws Exception {
if (autoPkTableExists(node)) {
runUpdate(node, dropAutoPkString());
}
}
public List dropAutoPkStatements(List dbEntities) {
List list = new ArrayList();
list.add(dropAutoPkString());
return list;
}
protected String pkTableCreateString() {
StringBuffer buf = new StringBuffer();
buf
.append("CREATE TABLE AUTO_PK_SUPPORT (")
.append(" TABLE_NAME CHAR(100) NOT NULL,")
.append(" NEXT_ID INTEGER NOT NULL,")
.append(" PRIMARY KEY(TABLE_NAME)")
.append(")");
return buf.toString();
}
protected String pkDeleteString(List dbEntities) {
StringBuffer buf = new StringBuffer();
buf.append("DELETE FROM AUTO_PK_SUPPORT WHERE TABLE_NAME IN (");
int len = dbEntities.size();
for (int i = 0; i < len; i++) {
if (i > 0) {
buf.append(", ");
}
DbEntity ent = (DbEntity) dbEntities.get(i);
buf.append('\'').append(ent.getName()).append('\'');
}
buf.append(')');
return buf.toString();
}
protected String pkCreateString(String entName) {
StringBuffer buf = new StringBuffer();
buf
.append("INSERT INTO AUTO_PK_SUPPORT")
.append(" (TABLE_NAME, NEXT_ID)")
.append(" VALUES ('")
.append(entName)
.append("', 200)");
return buf.toString();
}
protected String pkSelectString(String entName) {
StringBuffer buf = new StringBuffer();
buf.append("SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = '").append(
entName).append('\'');
return buf.toString();
}
protected String pkUpdateString(String entName) {
StringBuffer buf = new StringBuffer();
buf.append("UPDATE AUTO_PK_SUPPORT").append(" SET NEXT_ID = NEXT_ID + ").append(
pkCacheSize).append(" WHERE TABLE_NAME = '").append(entName).append('\'');
return buf.toString();
}
protected String dropAutoPkString() {
return "DROP TABLE AUTO_PK_SUPPORT";
}
/**
* Checks if AUTO_PK_TABLE already exists in the database.
*/
protected boolean autoPkTableExists(DataNode node) throws SQLException {
Connection con = node.getDataSource().getConnection();
boolean exists = false;
try {
DatabaseMetaData md = con.getMetaData();
ResultSet tables = md.getTables(null, null, "AUTO_PK_SUPPORT", null);
try {
exists = tables.next();
}
finally {
tables.close();
}
}
finally {
// return connection to the pool
con.close();
}
return exists;
}
/**
* Runs JDBC update over a Connection obtained from DataNode. Returns a number of
* objects returned from update.
*
* @throws SQLException in case of query failure.
*/
public int runUpdate(DataNode node, String sql) throws SQLException {
QueryLogger.logQuery(sql, Collections.EMPTY_LIST);
Connection con = node.getDataSource().getConnection();
try {
Statement upd = con.createStatement();
try {
return upd.executeUpdate(sql);
}
finally {
upd.close();
}
}
finally {
con.close();
}
}
/**
* @deprecated Since 1.2 corresponding interface method is deprecated.
*/
public String generatePkForDbEntityString(DbEntity ent) {
StringBuffer buf = new StringBuffer();
buf.append(pkSelectString(ent.getName())).append('\n').append(
pkUpdateString(ent.getName()));
return buf.toString();
}
/**
*
* Generates new (unique and non-repeating) primary key for specified dbEntity.
*
*
* This implementation is naive since it does not lock the database rows when
* executing select and subsequent update. Adapter-specific implementations are more
* robust.
*
*/
public Object generatePkForDbEntity(DataNode node, DbEntity ent) throws Exception {
// check for binary pk
Object binPK = binaryPK(ent);
if (binPK != null) {
return binPK;
}
DbKeyGenerator pkGenerator = ent.getPrimaryKeyGenerator();
int cacheSize;
if (pkGenerator != null && pkGenerator.getKeyCacheSize() != null)
cacheSize = pkGenerator.getKeyCacheSize().intValue();
else
cacheSize = pkCacheSize;
// if no caching, always generate fresh
if (cacheSize <= 1) {
return new Integer(pkFromDatabase(node, ent));
}
synchronized (pkCache) {
PkRange r = (PkRange) pkCache.get(ent.getName());
if (r == null) {
// created exhaused PkRange
r = new PkRange(1, 0);
pkCache.put(ent.getName(), r);
}
if (r.isExhausted()) {
int val = pkFromDatabase(node, ent);
r.reset(val, val + cacheSize - 1);
}
return r.getNextPrimaryKey();
}
}
/**
* @return a binary PK if DbEntity has a BINARY or VARBINARY pk, null otherwise. This
* method will likely be deprecated in 1.1 in favor of a more generic
* soultion.
* @since 1.0.2
*/
protected byte[] binaryPK(DbEntity entity) {
List pkColumns = entity.getPrimaryKey();
if (pkColumns.size() == 1) {
DbAttribute pk = (DbAttribute) pkColumns.get(0);
if (pk.getMaxLength() > 0
&& (pk.getType() == Types.BINARY || pk.getType() == Types.VARBINARY)) {
return IDUtil.pseudoUniqueSecureByteSequence(pk.getMaxLength());
}
}
return null;
}
/**
* Performs primary key generation ignoring cache. Generates a range of primary keys
* as specified by "pkCacheSize" bean property.
*
* This method is called internally from "generatePkForDbEntity" and then generated
* range of key values is saved in cache for performance. Subclasses that implement
* different primary key generation solutions should override this method, not
* "generatePkForDbEntity".
*
*/
protected int pkFromDatabase(DataNode node, DbEntity ent) throws Exception {
String select = "SELECT #result('NEXT_ID' 'int' 'NEXT_ID') "
+ "FROM AUTO_PK_SUPPORT "
+ "WHERE TABLE_NAME = '"
+ ent.getName()
+ '\'';
// run queries via DataNode to utilize its transactional behavior
List queries = new ArrayList(2);
queries.add(new SQLTemplate(ent, select));
queries.add(new SQLTemplate(ent, pkUpdateString(ent.getName())));
PkRetrieveProcessor observer = new PkRetrieveProcessor(ent.getName());
node.performQueries(queries, observer);
return observer.getId();
}
/**
* Returns a size of the entity primary key cache. Default value is 20. If cache size
* is set to a value less or equals than "one", no primary key caching is done.
*/
public int getPkCacheSize() {
return pkCacheSize;
}
/**
* Sets the size of the entity primary key cache. If pkCacheSize
* parameter is less than 1, cache size is set to "one".
*
* Note that our tests show that setting primary key cache value to anything much
* bigger than 20 does not give any significant performance increase. Therefore it
* does not make sense to use bigger values, since this may potentially create big
* gaps in the database primary key sequences in cases like application crashes or
* restarts.
*
*/
public void setPkCacheSize(int pkCacheSize) {
this.pkCacheSize = (pkCacheSize < 1) ? 1 : pkCacheSize;
}
public void reset() {
pkCache.clear();
}
/**
* OperationObserver for primary key retrieval.
*/
final class PkRetrieveProcessor implements OperationObserver {
Number id;
String entityName;
PkRetrieveProcessor(String entityName) {
this.entityName = entityName;
}
/**
* @deprecated since 1.2 super is deprecated
*/
public Level getLoggingLevel() {
return null;
}
public boolean isIteratedResult() {
return false;
}
public int getId() {
if (id == null) {
throw new CayenneRuntimeException("No key was retrieved for entity "
+ entityName);
}
return id.intValue();
}
public void nextDataRows(Query query, List dataRows) {
// process selected object, issue an update query
if (dataRows == null || dataRows.size() == 0) {
throw new CayenneRuntimeException(
"Error generating PK : entity not supported: " + entityName);
}
if (dataRows.size() > 1) {
throw new CayenneRuntimeException(
"Error generating PK : too many rows for entity: " + entityName);
}
Map lastPk = (Map) dataRows.get(0);
id = (Number) lastPk.get("NEXT_ID");
}
public void nextCount(Query query, int resultCount) {
if (resultCount != 1) {
throw new CayenneRuntimeException("Error generating PK for entity '"
+ entityName
+ "': update count is wrong - "
+ resultCount);
}
}
public void nextBatchCount(Query query, int[] resultCount) {
}
public void nextGeneratedDataRows(Query query, ResultIterator keysIterator) {
}
public void nextDataRows(Query q, ResultIterator it) {
}
public void nextQueryException(Query query, Exception ex) {
throw new CayenneRuntimeException("Error generating PK for entity '"
+ entityName
+ "'.", ex);
}
public void nextGlobalException(Exception ex) {
throw new CayenneRuntimeException("Error generating PK for entity: "
+ entityName, ex);
}
}
}