blob: d9ae7325dc98e80b1e8e682b45e5dd4a62d43dbd [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;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.util.Collection;
import org.apache.empire.commons.ClassUtils;
import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.Column;
import org.apache.empire.data.Record;
import org.apache.empire.db.DBRowSet.PartialMode;
import org.apache.empire.db.exceptions.InvalidKeyException;
import org.apache.empire.db.exceptions.NoPrimaryKeyException;
import org.apache.empire.db.expr.compare.DBCompareExpr;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.ItemNotFoundException;
import org.apache.empire.exceptions.NotSupportedException;
import org.apache.empire.exceptions.ObjectNotValidException;
import org.apache.empire.exceptions.UnspecifiedErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents a record from a database table, view or query
*
* The class provides methods to create, read, update and delete records
*
* If an Idendity-column (AUTOINC) is defined, the value will be set upon creation by the dbms to the next value
* If a Timestamp-column is defined the value will be automatically set and concurrent changes of the record will be detected
*
* If changes to the record are made, but a rollback on the connection is performed, the changes will be reverted (Rollback-Handling)
*
* The record is Serializable either if the provided DBContext is serializable, or if the Context is provided on deserialization in a derived class.
*/
public class DBRecord extends DBRecordBase
{
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DBRecord.class);
/**
* varArgs to Array
* @param values
* @return the key
*/
public static Object[] key(Object... values)
{
if (values.length==0)
throw new InvalidArgumentException("values", values);
// check values
for (int i=0; i<values.length; i++) {
// Replace record with key
if (values[i] instanceof Record)
values[i]=((Record)values[i]).getKey();
// Replace key with value
if (values[i] instanceof Object[]) {
Object[] key = (Object[])values[i];
if (key.length!=1)
throw new InvalidArgumentException("values", values[i]);
values[i]=key[0];
}
}
return values;
}
// Context and RowSet
protected final transient DBContext context; /* transient for serialization */
protected final transient DBRowSet rowset; /* transient for serialization */
// options
private boolean enableRollbackHandling;
/**
* Custom serialization for transient rowset.
*
*/
private void writeObject(ObjectOutputStream strm) throws IOException
{ // Context
writeContext(strm);
// RowSet
writeRowSet(strm);
// write object
strm.defaultWriteObject();
}
protected void writeContext(ObjectOutputStream strm) throws IOException
{
strm.writeObject(context);
}
protected void writeRowSet(ObjectOutputStream strm) throws IOException
{
String dbid = rowset.getDatabase().getIdentifier();
String rsid = rowset.getName();
strm.writeObject(dbid);
strm.writeObject(rsid);
}
/**
* Custom deserialization for transient rowset.
*/
private void readObject(ObjectInputStream strm) throws IOException, ClassNotFoundException
{ // Context
DBContext ctx = readContext(strm);
ClassUtils.setPrivateFieldValue(DBRecord.class, this, "context", ctx);
// set final field
DBRowSet rowset = readRowSet(strm);
ClassUtils.setPrivateFieldValue(DBRecord.class, this, "rowset", rowset);
// read the rest
strm.defaultReadObject();
}
protected DBContext readContext(ObjectInputStream strm) throws IOException, ClassNotFoundException
{
return (DBContext)strm.readObject();
}
protected DBRowSet readRowSet(ObjectInputStream strm) throws IOException, ClassNotFoundException
{ // Rowset
String dbid = String.valueOf(strm.readObject());
String rsid = String.valueOf(strm.readObject());
// find database
DBDatabase dbo = DBDatabase.findByIdentifier(dbid);
if (dbo==null)
throw new ItemNotFoundException(dbid);
// find rowset
DBRowSet rso = dbo.getRowSet(rsid);
if (rso==null)
throw new ItemNotFoundException(dbid);
// done
return rso;
}
/**
* Internal constructor for DBRecord
* May be used by derived classes to provide special behaviour
*/
protected DBRecord(DBContext context, DBRowSet rowset, boolean enableRollbackHandling)
{ // init
this.context = context;
this.rowset = rowset;
// options
this.enableRollbackHandling = enableRollbackHandling;
this.validateFieldValues = true;
}
/**
* Constructs a new DBRecord.<BR>
* @param context the DBContext for this record
* @param rowset the corresponding RowSet(Table, View, Query, etc.)
*/
public DBRecord(DBContext context, DBRowSet rowset)
{
this(checkParamNull("context", context),
checkParamNull("rowset", rowset),
context.isRollbackHandlingEnabled());
}
/**
* Returns the current Context
* @return
*/
@Override
public DBContext getContext()
{
if (this.context==null)
throw new ObjectNotValidException(this);
return context;
}
/**
* Returns the DBRowSet object.
*
* @return the DBRowSet object
*/
@Override
public DBRowSet getRowSet()
{
if (this.rowset==null)
throw new ObjectNotValidException(this);
return this.rowset;
}
/**
* Returns whether or not RollbackHandling is enabled for this record
*/
@Override
public boolean isRollbackHandlingEnabled()
{
return this.enableRollbackHandling;
}
/**
* Set whether or not RollbackHandling will be performed for this record
* Since Rollback handling requires additional resources it should only be used if necessary
* Especially for bulk operations it should be disabled
* @param enabled flag whether to enable or disable RollbackHandling
*/
public void setRollbackHandlingEnabled(boolean enabled)
{
// check
if (enabled && !getContext().isRollbackHandlingEnabled())
throw new UnspecifiedErrorException("Rollback handling cannot be enabled for this record since it is not supported for this context!");
// enable or disable
this.enableRollbackHandling = enabled;
}
/**
* Returns the record identity for tables which have a single numeric primary key like AUTOINC
* This method is provided for convenience in addition to the the getKey() method
* @return the record id or 0 if the key is null
* @throws NoPrimaryKeyException if the table has no primary key
* @throws NotSupportedException if the primary key is not a single column of if the column is not numeric
*/
public long getIdentity()
{
// Check Columns
Column[] keyColumns = getKeyColumns();
if (keyColumns == null || keyColumns.length==0)
throw new NoPrimaryKeyException(getRowSet());
// Check Columns
if (keyColumns.length!=1 || !keyColumns[0].getDataType().isNumeric())
throw new NotSupportedException(this, "getIdentity");
// the numeric id
return getLong(keyColumns[0]);
}
/**
* Creates a new record
*/
public DBRecord create(Object[] initalKey)
{
getRowSet().createRecord(this, initalKey, true);
return this;
}
/**
* Creates a new record
*/
public DBRecord create()
{
getRowSet().createRecord(this, null, false);
return this;
}
/**
* Reads a record from the database
* @param key an array of the primary key values
*
* @throws NoPrimaryKeyException if the associated RowSet has no primary key
* @throws InvalidKeyException if the key does not match the key columns of the associated RowSet
*/
public DBRecord read(Object[] key)
{ // read
DBRowSet rs = getRowSet();
DBCompareExpr keyConstraints = rs.getKeyConstraints(key);
rs.readRecord(this, keyConstraints);
return this;
}
/**
* Reads a record from the database
* This method can only be used for tables with a single primary key
* @param id the primary key of the record
*
* @throws NoPrimaryKeyException if the associated RowSet has no primary key
* @throws InvalidKeyException if the associated RowSet does not have a single column primary key
*/
public DBRecord read(Object id)
{
if (ObjectUtils.isEmpty(id))
throw new InvalidArgumentException("id", id);
// convert to array
Object[] key;
if (id instanceof Object[]) {
// Cast to array
key = (Object[])id;
} else if (id instanceof Collection<?>) {
// Convert collection to array
key = ((Collection<?>)id).toArray();
} else {
// Single value
key = new Object[] { id };
}
return read(key);
}
/**
* Reads a record from the database
* @param key an array of the primary key values
*/
public DBRecord read(DBCompareExpr whereConstraints)
{
getRowSet().readRecord(this, whereConstraints);
return this;
}
/**
* Reads a record partially i.e. not with all but just some selected fields
* There are two modes:
* 1. PartialMode.INCLUDE reads only the fields provided with the column list
* 2. PartialMode.EXCLUDE reads all but the fields provided with the column list
* The primary key is always fetched implicitly
* @param key the primary key values
* @param mode flag whether to include only the given columns or whether to add all but the given columns
* @param columns the columns to include or exclude (depending on mode)
*/
public DBRecord read(Object[] key, PartialMode mode, DBColumn... columns)
{
DBRowSet rs = getRowSet();
DBCompareExpr keyConstraints = rs.getKeyConstraints(key);
rs.readRecord(this, keyConstraints, mode, columns);
return this;
}
/**
* Sets the value of a column in the record.
* Same as getValue but provided in conjunction with set(...)
* @param column a DBColumn object
* @param value the value
public final Object get(Column column)
{
return getValue(column);
}
*/
/**
* Overridden to change return type from DBCommandExpr to DBCommand
*/
@Override
public DBRecord set(Column column, Object value)
{
return (DBRecord)super.set(column, value);
}
/**
* Updates the record and saves all changes in the database.
*/
public void update()
{
if (!isValid())
throw new ObjectNotValidException(this);
if (!isModified())
return; /* Not modified. Nothing to do! */
// check updatable
checkUpdateable();
// allow rollback
if (isRollbackHandlingEnabled())
getContext().appendRollbackHandler(createRollbackHandler());
// set parent record identity
assignParentIdentities();
// update
getRowSet().updateRecord(this);
}
/**
* This helper function calls the DBRowset.deleteRecord method
* to delete the record.
*
* WARING: There is no guarantee that it ist called
* Implement delete logic in the table's deleteRecord method if possible
*
* @see org.apache.empire.db.DBTable#deleteRecord(Object[], Connection)
* @param conn a valid connection to the database.
*/
public void delete()
{
if (isValid()==false)
throw new ObjectNotValidException(this);
// check updatable
checkUpdateable();
// allow rollback
if (isRollbackHandlingEnabled())
getContext().appendRollbackHandler(createRollbackHandler());
// Delete only if record is not new
if (!isNew())
{ // Delete existing record
Object[] key = getKey();
log.info("Deleting record {}", StringUtils.arrayToString(key, "|"));
getRowSet().deleteRecord(key, getContext());
}
close();
}
}