blob: 1fa0f80ee6a2ee34f92d1d3276c27a1756dce655 [file] [log] [blame]
/*
* 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.empire.db;
// java
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.empire.commons.Options;
import org.apache.empire.data.DataMode;
import org.apache.empire.data.DataType;
import org.apache.empire.db.DBIndex.DBIndexType;
import org.apache.empire.db.DBRelation.DBCascadeAction;
import org.apache.empire.db.exceptions.NoPrimaryKeyException;
import org.apache.empire.db.exceptions.RecordDeleteFailedException;
import org.apache.empire.db.exceptions.RecordUpdateInvalidException;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.ItemExistsException;
import org.apache.empire.exceptions.UnexpectedReturnValueException;
/**
* This class represent one table of the database.
* It contains methods to get, add, update and delete records from the database.
*
*/
public class DBTable extends DBRowSet implements Cloneable
{
// Integer size definitions
public static final int DEFAULT = 0;
public static final int SMALLINT = 2;
public static final int MEDIUMINT = 4;
public static final int BIGINT = 8;
private final static long serialVersionUID = 1L;
private static AtomicInteger tableCount = new AtomicInteger(0);
private final String name;
private String alias;
private final List<DBIndex> indexes = new ArrayList<DBIndex>();
private Boolean quoteName = null;
private DBCascadeAction cascadeDeleteAction = DBCascadeAction.NONE;
/**
* Construct a new DBTable object set the specified parameters
* to this object and add this object to the current database.
*
* @param name the table name
* @param db the valid database object
*/
public DBTable(String name, DBDatabase db, String alias)
{
super(db);
// generate alias
if (alias==null)
alias = "t" + String.valueOf(tableCount.incrementAndGet());
// init
this.name = name;
this.alias = alias;
// Add Table to Database
if (db != null)
db.addTable(this);
}
/**
* Construct a new DBTable object set the specified parameters
* to this object and add this object to the current database.
*
* @param name the table name
* @param db the valid database object
*/
public DBTable(String name, DBDatabase db)
{
this(name, db, null);
}
/**
* Returns the table name of this object.
*
* @return the table name of this object
*/
@Override
public String getName()
{
return name;
}
/**
* Returns the table alias name of this object.
*
* @return the table alias name of this object
*/
@Override
public String getAlias()
{
return alias;
}
/**
* Returns whether or not the table supports record updates. Default is true.
* @return true if the table allows record updates
*/
@Override
public boolean isUpdateable()
{
return true;
}
/**
* Clones this table and assigns a new table alias.
* This second instance of the same table can be used for self-joins.
* <pre>
* This method requires that all declared column fields are NOT declared final.
* i.e. instead of:
*
* public final DBTableColumn MYCOL;
*
* columns must be declared:
*
* public DBTableColumn MYCOL;
*
* A runtime exception for the CloneNotSupported will be thrown if references cannot be adjusted.
*
* Alternatively a second table instance may be created manually like this:
*
* public final MyTable MYTABLE1 = new MyTable();
* public final MyTable MYTABLE2 = new MyTable();
*
* ...
* cmd.join(MYTABLE1.ID, MYTABLE2.PARENTID); // self-join
* ...
* </pre>
* @return a table clone with new table alias
*/
@Override
public Object clone() throws CloneNotSupportedException {
try {
DBTable clone = (DBTable) super.clone();
// clone all columns
Class<?> colClass = columns.get(0).getClass();
Class<?> colBase = colClass.getSuperclass();
clone.columns = new ArrayList<DBColumn>();
Field[] fields = getClass().getFields();
for (int i = 0; i < columns.size(); i++)
{
DBTableColumn srcCol = (DBTableColumn) columns.get(i);
DBTableColumn newCol = new DBTableColumn(clone, srcCol);
// Replace all references for oldCol to newCol
for (int j = 0; j < fields.length; j++)
{ // Find a class of Type DBColumn or DBTableColumn
Class<?> type = fields[j].getType();
if (type == colClass || type == colBase)
{
try
{ // Check if the field points to the old Value
if (fields[j].get(clone) == srcCol)
fields[j].set(clone, newCol);
} catch (Exception e) {
// IllegalAccessException or IllegalArgumentException
String fieldName = fields[j].getName();
log.error("Cannot adjust declared table field: " + fieldName + ". Reason is: " + e.getMessage());
// throw CloneNotSupportedException
CloneNotSupportedException cnse = new CloneNotSupportedException("Unable to replace field reference for field " + fieldName);
cnse.initCause(e);
throw cnse;
}
}
}
}
// set new alias
clone.alias = "t" + String.valueOf(tableCount.incrementAndGet());
// done
log.info("clone: Table " + name + " cloned! Alias old=" + alias + " new=" + clone.alias);
return clone;
} catch (CloneNotSupportedException e) {
// unable to clone table
log.error("Unable to clone table " + getName());
throw new RuntimeException(e);
}
}
/**
* Adds a column to this table's column list.
* @param column a column object
*/
protected void addColumn(DBTableColumn column)
{ // find column by name
if (column==null || column.getRowSet()!=this)
throw new InvalidArgumentException("column", column);
if (getColumn(column.getName())!=null)
throw new ItemExistsException(column.getName());
// add now
columns.add(column);
}
/**
* Creates a new Column object and appends it to the column list
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @param defValue a Object object
* @return the new column object
*/
protected DBTableColumn crateAndAppendColumn(String columnName, DataType type, double size, boolean required, Object defValue)
{
// Make sure (DataType.INTEGER & DataMode.AutoGenerated) = DataType.AUTOINC
boolean autoGenerated = (type==DataType.AUTOINC || type==DataType.UNIQUEID);
DBTableColumn column = new DBTableColumn(this, type, columnName, size, required, autoGenerated, defValue);
addColumn(column);
// auto-set primary key
if (column.getDataType()==DataType.AUTOINC)
{ // Automatically set primary key
if (this.primaryKey==null)
this.setPrimaryKey(column);
else
log.warn("Table {} already has a Primary-Key! DataType of column {} should be INTEGER.", getName(), column.getName());
}
// auto-set timestamp column
if (column.getDataType()==DataType.TIMESTAMP)
{ // Automatically set timestamp column
if (timestampColumn==null)
this.setTimestampColumn(column);
else
log.warn("Table {} already has a Timestamp column. DataType of column {} should be DATETIME.", getName(), column.getName());
}
return column;
}
/**
* Deprecacted function to add a table column.
* This function will be removed in a further release.
* @deprecated use {@link #addColumn(String columnName, DataType type, double size, boolean required, Object defValue)} instead.
* @return the new column object
*/
@Deprecated
public final DBTableColumn addColumn(String columnName, DataType type, double size, DataMode dataMode, Object defValue)
{
return this.addColumn(columnName, type, size, (dataMode!=DataMode.Nullable), defValue);
}
/**
* Deprecacted function to add a table column
* This function will be removed in a further release.
* @deprecated use {@link #addColumn(String columnName, DataType type, double size, boolean required)} instead.
* @return the new column object
*/
@Deprecated
public final DBTableColumn addColumn(String columnName, DataType type, double size, DataMode dataMode)
{
return this.addColumn(columnName, type, size, (dataMode!=DataMode.Nullable));
}
/**
* Creates a new DBTableColumn object and adds it to the column collection.
* Instead of the data mode enum, a boolean flag is used to indicate whether the column is required or optional.
*
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @param defValue a Object object
* @return the new column object
*/
public final DBTableColumn addColumn(String columnName, DataType type, double size, boolean required, Object defValue)
{
if (defValue instanceof Class<?>)
{
log.warn("Column {}: a class object of type \"{}\" has been passed as default value. Please check!", columnName, ((Class<?>)defValue).getName());
}
return this.crateAndAppendColumn(columnName, type, size, required, defValue);
}
/**
* Creates a new table column and adds it to the table's column list
*
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @return the new column object
*/
public final DBTableColumn addColumn(String columnName, DataType type, double size, boolean required)
{
return this.crateAndAppendColumn(columnName, type, size, required, null);
}
/**
* Creates a new table column with options and adds it to the table's column list
* This overload should be used for column containing enum values which have no default value.
*
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @param options this list of options
* @return the new column object
*/
public final DBTableColumn addColumn(String columnName, DataType type, double size, boolean required, Options options)
{
DBTableColumn col = this.crateAndAppendColumn(columnName, type, size, required, null);
col.setOptions(options);
return col;
}
/**
* Creates a new table column with options and adds it to the table's column list
* This overload should be used for column containing enum values which have a default value.
*
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @param options this list of options
* @param defValue the default value
* @return the new column object
*/
public final DBTableColumn addColumn(String columnName, DataType type, double size, boolean required, Options options, Object defValue)
{
// defValue must be part of options
if (defValue!=null && !options.contains(defValue))
throw new InvalidArgumentException("devValue", defValue);
// add
DBTableColumn col = this.crateAndAppendColumn(columnName, type, size, required, defValue);
col.setOptions(options);
return col;
}
/**
* Creates a new table column with Enum-Options and adds it to the table's column list
* This overload should be used for column containing enum values which have no default value.
*
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @param enumType the class of the enum type
* @return the new column object
*/
public final DBTableColumn addColumn(String columnName, DataType type, double size, boolean required, Class<?> enumType)
{
if (!enumType.isEnum())
{ // Class must be an enum type
throw new InvalidArgumentException("enumType", enumType);
}
DBTableColumn col = this.crateAndAppendColumn(columnName, type, size, required, null);
col.setEnumOptions(enumType);
return col;
}
/**
* Creates a new table column with Enum-Options and adds it to the table's column list
* This overload should be used for column containing enum values which have a default value.
*
* @param columnName the column name
* @param type the type of the column e.g. integer, text, date
* @param size the column width
* @param required true if not null column
* @param enumType defValue the default value
* @return the new column object
*/
public final DBTableColumn addColumn(String columnName, DataType type, double size, boolean required, Enum<?> enumValue)
{
Object defValue = type.isNumeric() ? enumValue.ordinal() : enumValue.name();
DBTableColumn col = this.crateAndAppendColumn(columnName, type, size, required, defValue);
col.setEnumOptions(enumValue.getClass());
return col;
}
/**
* Returns the primary key.
*
* @return the the DBIndex object -&gt; primary key
*/
public DBIndex getPrimaryKey()
{
return primaryKey;
}
/**
* Returns the list of indexes (except the primary key).
*
* @return a list of DBIndex objects
*/
public List<DBIndex> getIndexes()
{
return Collections.unmodifiableList(this.indexes);
}
/**
* Sets the primary keys.
*
* @param columns a array with one or more DBColumn objects
*/
public void setPrimaryKey(DBColumn... columns)
{
if (columns==null)
throw new InvalidArgumentException("columns", columns);
// All columns must belong to this table
for (int i=0; i<columns.length; i++)
if (columns[i].getRowSet()!=this)
throw new InvalidArgumentException("columns["+String.valueOf(i)+"]", columns[i].getFullName());
// Check if already exists
if (primaryKey!=null)
{ // compare columns
if (primaryKey.compareColumns(columns))
return; // already set
// new key
removeIndex(primaryKey);
}
// Set primary Key now
if (columns.length>0)
{ // create primary key
primaryKey = new DBIndex(name + "_PK", DBIndexType.PRIMARY_KEY, columns);
addIndex(primaryKey);
}
else
{ // No primary Key
primaryKey = null;
}
}
/**
* Adds an index.
*
* @param index the index to add
*/
public DBIndex addIndex(DBIndex index)
{
if (index==null)
throw new InvalidArgumentException("index", null);
// Check index name
String name = index.getName();
for (DBIndex i : indexes)
{
if (i==index || name.equalsIgnoreCase(i.getName()))
{
throw new ItemExistsException(name);
}
}
// add Index now
indexes.add(index);
index.setTable(this);
return index;
}
/**
* Adds an index.
*
* @param name the index name
* @param unique is this a unique index
* @param columns the columns indexed by this index
*
* @return the Index object
*/
public final DBIndex addIndex(String name, DBIndexType type, DBColumn... columns)
{
if (name==null || columns==null || columns.length==0)
throw new InvalidArgumentException("name|columns", null);
if (type==DBIndexType.PRIMARY_KEY && this.primaryKey!=null)
throw new InvalidArgumentException("type", DBIndexType.PRIMARY_KEY.name());
// add Index now
DBIndex index = new DBIndex(name, type, columns);
addIndex(index);
return index;
}
/**
* Adds an index.
* Overload for convenience
*/
public final DBIndex addIndex(String name, boolean unique, DBColumn... columns)
{
return addIndex(name, (unique) ? DBIndexType.UNIQUE : DBIndexType.STANDARD, columns);
}
/**
* removes an index.
*
* @param index the index to remove
*/
public void removeIndex(DBIndex index)
{
if (index.getTable()!=this || !indexes.contains(index))
throw new InvalidArgumentException("index", index);
// table
indexes.remove(index);
index.setTable(null);
}
/**
* Adds a timestamp column to the table used for optimistic locking.
*
* @param columnName the column name
*
* @return the timestamp table column object
*/
public DBTableColumn addTimestampColumn(String columnName)
{
DBTableColumn col = addColumn(columnName, DataType.TIMESTAMP, 0, true, DBDatabase.SYSDATE);
if (this.timestampColumn!=col)
setTimestampColumn(col); // make sure, this is the timestamp column, even if another one exists
return col;
}
/**
* Adds the table's name to the supplied sql command buffer.
*
* @param buf the SQL-Command
* @param context the current SQL-Command context
*/
@Override
public void addSQL(StringBuilder buf, long context)
{
// Append Name
if ((context & CTX_NAME|CTX_FULLNAME)!=0)
{ // Append the name
DBDatabaseDriver driver = getDatabase().getDriver();
if (quoteName==null)
quoteName = driver.detectQuoteName(name);
// append Qualified Name
db.appendQualifiedName(buf, name, quoteName);
}
// Append Alias
if ((context & CTX_ALIAS)!=0 && alias!=null)
{ // append alias
buf.append(getRenameTablePhrase());
buf.append(getAlias());
}
}
/**
* Gets all table fields and the fields properties.
* Set this to the specified DBRecord object.
*
* @param rec the DBRecord object. contains all fields and the field properties
* @param conn a valid connection to the database.
*/
@Override
public void createRecord(DBRecord rec, Connection conn)
{
// Prepare
prepareInitRecord(rec, null, true);
// Set Defaults
int count = columns.size();
for (int i = 0; i < count; i++)
{
DBTableColumn column = (DBTableColumn)columns.get(i);
Object value = column.getRecordDefaultValue(conn);
if (value!=null)
rec.modifyValue(i, value, true);
}
// Init
completeInitRecord(rec);
}
/**
* Checks weather a unique constraint is violated when inserting or updating a record.<BR>
* <P>
* @param id the record's primary key
* @param conn a valid JDBC connection
*/
public DBIndex checkUniqueConstraints(DBRecord rec, Connection conn)
{
for (DBIndex idx : getIndexes())
{
if (idx.getType()==DBIndexType.PRIMARY_KEY)
{ // Only for new records
if (!rec.isNew())
continue; // not new
}
else if (idx.getType().isUnique())
{ // check if any of the fields were actually changed
if (!rec.isNew() && !rec.wasAnyModified(idx.getColumns()))
continue; // not modified
}
else
{ // No unique index
continue;
}
// Check index
DBCommand cmd = db.createCommand();
cmd.select(count());
for (DBColumn c : idx.getColumns())
{
Object value = rec.getValue(c);
cmd.where(c.is(value));
}
int count = db.querySingleInt(cmd, conn);
if (count>0)
{ // Index is violated
return idx;
}
}
// no index violation detected
return null;
}
/**
* returns the default cascade action for deletes on this table.
* This is used as the default for newly created relations on this table and does not affect existing relations.
* @return the delete cascade action for new relations (DBRelation.DBCascadeAction.CASCADE_RECORDS) are enabled
*/
public DBCascadeAction getDefaultCascadeDeleteAction()
{
return cascadeDeleteAction;
}
/**
* sets the default cascade action for deletes on foreign key relations.
* @param cascadeDeleteAction cascade action for deletes (DBRelation.DBCascadeAction.CASCADE_RECORDS)
*/
public void setDefaultCascadeDeleteAction(DBCascadeAction cascadeDeleteAction)
{
this.cascadeDeleteAction = cascadeDeleteAction;
}
/**
* Creates a record key from a list of key values.
* The supplied values must be in the correct order.
* @param keyValues
* @return the record key
*/
public Object[] key(Object... keyValues)
{ // Check size
if (keyValues==null || keyValues.length==0)
throw new InvalidArgumentException("keyValues", keyValues);
if (this.primaryKey!=null && keyValues.length!=this.primaryKey.getColumnCount())
throw new InvalidArgumentException("keyValues:length", keyValues.length);
// Return the key
return keyValues;
}
/**
* Creates a delete SQL-Command by using the DBCommand getDelete method
* execute the the SQL-Command with the DBDatabase
* executeSQL method.
*
* @param key an array of the primary key columns
* @param conn a valid connection to the database.
*/
@Override
public void deleteRecord(Object[] key, Connection conn)
{
// Check Primary key
if (primaryKey == null )
throw new NoPrimaryKeyException(this);
// Check Columns
DBColumn[] keyColumns = primaryKey.getColumns();
if (key == null || key.length != keyColumns.length)
throw new InvalidArgumentException("key", key);
// Delete References
deleteAllReferences(key, conn);
// Build SQL-Statement
DBCommand cmd = db.createCommand();
// Set key constraints
setKeyConstraints(cmd, key);
// Perform delete
String sqlCmd = cmd.getDelete(this);
int affected = db.executeSQL(sqlCmd, cmd.getParamValues(), conn);
if (affected < 0)
{ // Delete Failed
throw new UnexpectedReturnValueException(affected, "db.executeSQL()");
}
else if (affected == 0)
{ // Record not found
throw new RecordDeleteFailedException(this, key);
}
else if (affected > 1)
{ // Multiple Records affected
throw new RecordUpdateInvalidException(this, key);
}
}
/**
* Returns a list of all foreign key relations for this table
* @return the list of foreign key relations
*/
public List<DBRelation> getForeignKeyRelations()
{
List<DBRelation> relations = new ArrayList<DBRelation>();
for (DBRelation r : getDatabase().getRelations())
{ // check relation
if (this.equals(r.getForeignKeyTable()))
relations.add(r);
}
return Collections.unmodifiableList(relations);
}
}