| /* |
| * 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.sql.Connection; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.empire.commons.ObjectUtils; |
| import org.apache.empire.commons.StringUtils; |
| import org.apache.empire.data.Column; |
| import org.apache.empire.data.ColumnExpr; |
| import org.apache.empire.data.DataType; |
| import org.apache.empire.data.EntityType; |
| import org.apache.empire.data.Record; |
| import org.apache.empire.db.DBRelation.DBCascadeAction; |
| import org.apache.empire.db.DBRelation.DBReference; |
| import org.apache.empire.db.exceptions.FieldNotNullException; |
| import org.apache.empire.db.exceptions.FieldReadOnlyException; |
| import org.apache.empire.db.exceptions.InvalidKeyException; |
| import org.apache.empire.db.exceptions.NoPrimaryKeyException; |
| import org.apache.empire.db.exceptions.QueryNoResultException; |
| import org.apache.empire.db.exceptions.RecordNotFoundException; |
| import org.apache.empire.db.exceptions.RecordUpdateAmbiguousException; |
| import org.apache.empire.db.exceptions.RecordUpdateFailedException; |
| import org.apache.empire.db.expr.column.DBCountExpr; |
| import org.apache.empire.db.expr.compare.DBCompareColExpr; |
| import org.apache.empire.db.expr.compare.DBCompareExpr; |
| import org.apache.empire.db.list.DBBeanFactoryCache; |
| import org.apache.empire.db.list.DBBeanListFactory; |
| import org.apache.empire.db.list.DBBeanListFactoryImpl; |
| import org.apache.empire.dbms.DBMSFeature; |
| import org.apache.empire.dbms.DBMSHandler; |
| import org.apache.empire.dbms.DBSqlPhrase; |
| 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.UnexpectedReturnValueException; |
| import org.apache.empire.exceptions.UnspecifiedErrorException; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| |
| /** |
| * This class is the base class for all the DBTable, |
| * DBView and DBQuery classes this class contains all the columns of the |
| * tables, views or queries |
| * <P> |
| * |
| * |
| */ |
| public abstract class DBRowSet extends DBExpr implements EntityType |
| { |
| // *Deprecated* private static final long serialVersionUID = 1L; |
| |
| public enum PartialMode |
| { |
| INCLUDE, |
| EXCLUDE; |
| } |
| |
| public enum FieldInitMode |
| { |
| NONE, |
| SET_DEFAULTS, |
| SET_DEFAULTS_DEFERRED; |
| } |
| |
| /** |
| * This class is used to set the auto generated key of a record if the database does not support sequences. |
| * It is used with the executeSQL function and only required for insert statements |
| */ |
| private static class DBSetRecordKey implements DBMSHandler.DBSetGenKeys |
| { |
| private Object[] fields; |
| private int index; |
| public DBSetRecordKey(Object[] fields, int index) |
| { |
| this.fields = fields; |
| this.index = index; |
| } |
| @Override |
| public void set(int rownum, Object value) |
| { |
| fields[index]=value; |
| } |
| } |
| |
| // Logger |
| protected static final Logger log = LoggerFactory.getLogger(DBRowSet.class); |
| |
| /* |
| * Do we need a BeanRowsetMap? |
| * |
| private static final Map<Class<?>, DBRowSet> beanRowsetMap = new HashMap<Class<?>, DBRowSet>(); // Concurrent ? |
| |
| public static synchronized DBRowSet getRowsetforType(Class<?> beanType, boolean checkExists) |
| { |
| DBRowSet rowset = beanRowsetMap.get(beanType); |
| if (rowset==null && checkExists) |
| throw new UnknownBeanTypeException(beanType); |
| return rowset; |
| } |
| |
| public static synchronized void setRowsetForType(Class<?> beanType, DBRowSet rowset) |
| { |
| if (rowset!=null) |
| { // Check previous |
| DBRowSet prev = beanRowsetMap.get(beanType); |
| if (prev!=null && prev!=rowset) |
| log.warn("The Java bean type '{}' has already been assigned to a different DBRowSet {}. Assiging now to {}", beanType.getName(), prev.getName(), rowset.getName()); |
| // Assign now |
| beanRowsetMap.put(beanType, rowset); |
| } |
| else |
| beanRowsetMap.remove(beanType); |
| } |
| */ |
| |
| // Members |
| protected final DBDatabase db; /* transient ? */ |
| protected String comment = null; |
| protected String entityName = null; |
| protected DBColumn timestampColumn = null; |
| protected Map<DBColumn, DBColumn> columnReferences = null; |
| protected List<DBColumn> columns = new ArrayList<DBColumn>(); |
| |
| // associated Entity Bean Class (optional) |
| protected Class<?> beanType = null; |
| |
| /** |
| * Internally used for parameter checking |
| * @param name the paramter name |
| * @param record the record |
| * @return the record |
| */ |
| protected void checkParamRecord(DBRecordBase record, boolean checkValid) |
| { |
| // special |
| if ((record instanceof DBRecordBean) && !checkValid) |
| { // set rowset of DBRecordBean |
| ((DBRecordBean)record).rowset = this; |
| } |
| // check param |
| if (record==null || record.getRowSet()!=this) |
| throw new InvalidArgumentException("record", record); |
| // check valid |
| if (checkValid && !record.isValid()) |
| throw new ObjectNotValidException(record); |
| } |
| |
| /** |
| * varArgs to Array |
| * @param parts |
| * @return |
| */ |
| public static DBColumn[] key(DBColumn... parts) |
| { |
| return parts; |
| } |
| |
| /** |
| * Constructs a DBRecord object set the current database object. |
| * @param db the database object |
| */ |
| public DBRowSet(DBDatabase db) |
| { |
| this.db = db; |
| } |
| |
| /** |
| * Gets an identifier for this RowSet Object |
| * @return the rowset identifier |
| */ |
| public String getIdentifier() |
| { |
| return db.getIdentifier()+"."+getName(); |
| } |
| |
| /** |
| * returns a rowset by its identifier |
| * @param rowsetId the id of the rowset |
| * @return the rowset object |
| * |
| public static DBRowSet findByIdentifier(String rowsetId) |
| { |
| int i = rowsetId.lastIndexOf('.'); |
| if (i<0) |
| throw new InvalidArgumentException("rowsetId", rowsetId); |
| // database suchen |
| String dbid = rowsetId.substring(0, i); |
| DBDatabase db = DBDatabase.findByIdentifier(dbid); |
| if (db==null) |
| throw new ItemNotFoundException(dbid); |
| // rowset suchen |
| String rsname = rowsetId.substring(i+1); |
| DBRowSet rset = db.getRowSet(rsname); |
| if (rset==null) |
| throw new ItemNotFoundException(rowsetId); |
| return rset; |
| } |
| */ |
| |
| /** |
| * Custom serialization for transient database. |
| * |
| private void writeObject(ObjectOutputStream strm) throws IOException |
| { // Database |
| strm.writeObject(db.getIdentifier()); |
| // write the rest |
| strm.defaultWriteObject(); |
| } |
| |
| private void readObject(ObjectInputStream strm) throws IOException, ClassNotFoundException |
| { // Database |
| String dbid = String.valueOf(strm.readObject()); |
| // find database |
| DBDatabase dbo = DBDatabase.findByIdentifier(dbid); |
| if (dbo==null) |
| throw new ItemNotFoundException(dbid); |
| // set final field |
| ClassUtils.setPrivateFieldValue(DBRowSet.class, this, "db", dbo); |
| // read the rest |
| strm.defaultReadObject(); |
| } |
| */ |
| |
| @Override |
| public int hashCode() |
| { |
| String nameWithAlias = getFullName()+"_"+getAlias(); |
| return nameWithAlias.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object other) |
| { |
| if (other==this) |
| return true; |
| if (db==null) |
| return super.equals(other); |
| if (other instanceof DBRowSet) |
| { // Database and name must match |
| DBRowSet r = (DBRowSet) other; |
| if (db.equals(r.getDatabase())==false) |
| return false; |
| // Check Alias |
| if (getAlias()==null) |
| return super.equals(other); |
| // check for equal names |
| return StringUtils.compareEqual(getAlias(), r.getAlias(), true); |
| } |
| return false; |
| } |
| |
| /** |
| * Compares the rowset to another one |
| * @param otherObject |
| * @return true if it is the same rowset (but maybe a different instance) |
| */ |
| public boolean isSame(DBRowSet other) |
| { |
| // record |
| if (!getDatabase().equals(other.getDatabase())) |
| return false; |
| // compare names |
| return StringUtils.compareEqual(getName(), other.getName(), true); |
| } |
| |
| // ------- Abstract Methods ------- |
| |
| public abstract String getName(); |
| |
| public abstract String getAlias(); |
| |
| public abstract boolean isUpdateable(); |
| |
| public abstract void createRecord(DBRecordBase record, Object[] initalKey, boolean deferredInit); |
| |
| public abstract void deleteRecord(Object[] key, DBContext context); |
| |
| /** |
| * Returns the full qualified name of the rowset. |
| * <P> |
| * @return the full qualified name |
| */ |
| public String getFullName() |
| { |
| String name = getName(); |
| String schema = db.getSchema(); |
| return (schema!=null) ? schema+"."+name : name; |
| } |
| |
| /** |
| * Returns the entity name creating qualified names. |
| * This is usually the same as "getName()" but it may be overridden to return singular instead of plural |
| * @return the entity name |
| */ |
| @Override |
| public String getEntityName() |
| { |
| return StringUtils.coalesce(entityName, getName()); |
| } |
| |
| /** |
| * sets the entity name for creating qualified names. |
| * This is usefull if the table or view name is plural but you want to qualifiy names in singular |
| * @param entityName the entity name |
| */ |
| protected void setEntityName(String entityName) |
| { |
| this.entityName = entityName; |
| } |
| |
| /** |
| * returns the bean type for this rowset |
| * @return the bean type |
| */ |
| @Override |
| public Class<?> getBeanType() |
| { |
| return beanType; |
| } |
| |
| /** |
| * sets the bean type for this rowset |
| * @param beanType |
| */ |
| public void setBeanType(Class<?> beanType) |
| { |
| setBeanType(beanType, null); |
| } |
| |
| /** |
| * sets the bean type for this rowset |
| * @param beanType |
| */ |
| public <T> void setBeanType(Class<T> beanType, DBBeanListFactory<T> factory) |
| { |
| // set |
| this.beanType = beanType; |
| // set the entity name (if not already set) |
| if (this.entityName==null) |
| this.setEntityName(beanType.getSimpleName().toUpperCase()); |
| // create default factory if not provided |
| if (factory==null) |
| factory = new DBBeanListFactoryImpl<T>(beanType, getKeyColumns(), getColumns()); |
| // set to global map |
| DBBeanFactoryCache.setFactoryForType(beanType, factory); |
| } |
| |
| /** |
| * @see org.apache.empire.db.DBExpr#addReferencedColumns(Set) |
| */ |
| @Override |
| public void addReferencedColumns(Set<DBColumn> list) |
| { |
| list.addAll(columns); |
| } |
| |
| /** |
| * Returns the current DBDatabase object. |
| * <P> |
| * @return the current DBDatabase object |
| */ |
| @SuppressWarnings("unchecked") |
| @Override |
| public DBDatabase getDatabase() |
| { |
| return db; |
| } |
| |
| /** |
| * Gets all columns of this rowset (e.g. for cmd.select()). |
| * <P> |
| * @return all columns of this rowset |
| */ |
| @Override |
| public List<DBColumn> getColumns() |
| { |
| return Collections.unmodifiableList(columns); |
| } |
| |
| /** |
| * Gets the index of a particular column expression. |
| * <P> |
| * @param column column the DBColumn to get the index for |
| * |
| * @return the position of a column expression |
| */ |
| public int getColumnIndex(DBColumn column) |
| { |
| return columns.indexOf(column); |
| } |
| |
| /** |
| * Gets the index of a particular column expression. |
| * <P> |
| * @param column the Column to get the index for |
| * |
| * @return the position of a column expression |
| */ |
| public int getColumnIndex(ColumnExpr columnExpr) |
| { |
| if (columnExpr instanceof DBColumn) |
| return getColumnIndex((DBColumn)columnExpr); |
| else { |
| Column source = columnExpr.getSourceColumn(); |
| if (source instanceof DBColumn) |
| return getColumnIndex((DBColumn)source); |
| } |
| // not found |
| return -1; |
| } |
| |
| /** |
| * Returns a DBColumn object by a specified index value. |
| * |
| * @param iColumn the index to get the DBColumn for |
| * |
| * @return the index value |
| */ |
| public DBColumn getColumn(int iColumn) |
| { |
| if (iColumn < 0 || iColumn >= columns.size()) |
| return null; |
| return columns.get(iColumn); |
| } |
| |
| /** |
| * Gets the column Expression with a particular name. |
| * |
| * @param name the name of the column to look for |
| * |
| * @return the column Expression at position |
| */ |
| public DBColumn getColumn(String name) |
| { |
| for (int i = 0; i < columns.size(); i++) |
| { |
| DBColumn col = columns.get(i); |
| if (col.getName().equalsIgnoreCase(name)) |
| return col; |
| } |
| return null; |
| } |
| |
| /** |
| * Checks whether a column is read only or writable. |
| * Only the timestamp column is read only by default. |
| * The primary is read only if the column is of type. |
| * |
| * @param col the column object |
| * |
| * @return a new DBCountExpr object |
| */ |
| public boolean isColumnReadOnly(DBColumn col) |
| { |
| if (getColumnIndex(col)<0) |
| return true; // not found! |
| if (col.isAutoGenerated() || col==timestampColumn) |
| return true; // timestamp column |
| // Check Update Column |
| return (col.isReadOnly()); |
| } |
| |
| /** |
| * Checks whether a given column is part of the primary key for this RowSet |
| * @param column the column to check |
| * @return true if the column is part of the primary key or false otherwise |
| */ |
| public boolean isKeyColumn(DBColumn column) |
| { |
| Column[] keyColumns = getKeyColumns(); |
| return ObjectUtils.contains(keyColumns, column); |
| } |
| |
| /** |
| * @return Returns the comment. |
| */ |
| public String getComment() |
| { |
| return comment; |
| } |
| |
| /** |
| * @param comment The comment to set. |
| */ |
| public void setComment(String comment) |
| { |
| this.comment = comment; |
| } |
| |
| /** |
| * @return Returns the timestampColumn. |
| */ |
| public DBColumn getTimestampColumn() |
| { |
| return timestampColumn; |
| } |
| |
| /** |
| * @param timestampColumn The timestampColumn to set. |
| */ |
| public void setTimestampColumn(DBColumn timestampColumn) |
| { |
| if (timestampColumn!=null && timestampColumn.getRowSet()!=this) |
| throw new InvalidArgumentException("timestampColumn", timestampColumn); |
| if (timestampColumn!=null && this.timestampColumn!=null && this.timestampColumn!=timestampColumn) |
| throw new UnspecifiedErrorException("A Timestamp column has already been set for rowset "+getName()); |
| if (timestampColumn instanceof DBTableColumn) |
| ((DBTableColumn) timestampColumn).setReadOnly(true); |
| // set now |
| this.timestampColumn = timestampColumn; |
| // log |
| log.debug("Timestamp column {} has been set for table {}", timestampColumn.getName(), getName()); |
| } |
| |
| /** |
| * Returns the a list of column references. |
| * |
| * @return a list of references |
| */ |
| public Map<DBColumn, DBColumn> getColumnReferences() |
| { |
| return columnReferences; |
| } |
| |
| /** |
| * Adds a column reference to the list of table references. |
| * This method is internally called from DBDatabase.addReleation(). |
| * |
| * @param source a column reference for one of this table's column |
| * @param target the target column to which the source column references |
| */ |
| protected void addColumnReference(DBColumn source, DBColumn target) |
| { |
| if (source.getRowSet()!=this) |
| throw new InvalidArgumentException("column", source.getFullName()); |
| if (columnReferences== null) |
| columnReferences = new HashMap<DBColumn, DBColumn>(); |
| // Check if column is already there |
| columnReferences.put(source, target); |
| } |
| |
| /** |
| * Returns a new DBCountExpr object. |
| * |
| * @return a new DBCountExpr object |
| */ |
| public DBColumnExpr count() |
| { |
| return new DBCountExpr(this); |
| } |
| |
| /** |
| * Returns the sql phrase for renaming tables. |
| * usually just a space character ' ' |
| * |
| * @return the table rename phrase |
| */ |
| protected String getRenameTablePhrase() |
| { |
| if (db==null || db.getDbms()==null) |
| return " "; |
| return db.getDbms().getSQLPhrase(DBSqlPhrase.SQL_RENAME_TABLE); |
| } |
| |
| /** |
| * Returns the column expression at a given column index |
| * Allow overrides in derived classes |
| * @param index |
| * @return the column expression |
| */ |
| protected DBColumnExpr getColumnExprAt(int index) |
| { |
| return columns.get(index); |
| } |
| |
| /** |
| * Initializes a DBRecord for this RowSet and sets primary key values. |
| * The record may then be modified and updated.<BR> |
| * <P> |
| * @param record the Record object |
| * @param key an array of the values for the primary key |
| * @param fieldInitMode indicates how to initializes the record fields |
| * @param newRecord true if the record is new or false if it is an existing record |
| */ |
| protected void initRecord(DBRecordBase record, Object[] key, FieldInitMode fieldInitMode, boolean newRecord) |
| { |
| // check param |
| checkParamRecord(record, false); |
| // Prepare |
| prepareInitRecord(record, newRecord); |
| /* |
| * DO NOT fill with ObjectUtils.NO_VALUE! |
| */ |
| // Init Key Values |
| if (key != null) |
| { // Check Columns |
| DBColumn[] keyColumns =(DBColumn[])getKeyColumns(); |
| if (keyColumns==null) |
| throw new NoPrimaryKeyException(this); |
| if (key.length!=keyColumns.length) |
| throw new InvalidArgumentException("key", key); |
| // Set key |
| Object[] fields = record.getFields(); |
| for (int i = 0; i < keyColumns.length; i++) |
| { // ignore null (important!) |
| if (key[i]==null) |
| continue; |
| // check key column |
| DBColumn keyColumn = keyColumns[i]; |
| if (newRecord && keyColumn.isAutoGenerated()) |
| throw new FieldReadOnlyException(keyColumn); |
| // Ignore Validity Checks |
| int field = getColumnIndex(keyColumn); |
| fields[field] = key[i]; |
| } |
| } |
| // Set defaults (don't provide connection here) |
| if (fieldInitMode!=FieldInitMode.NONE) |
| { |
| initRecordDefaultValues(record, fieldInitMode); |
| } |
| // Init |
| completeInitRecord(record); |
| } |
| |
| /** |
| * Initializes a DBRecord for this rowset using the record data provided (i.e. from a DBReader)<BR> |
| * The record may then be modified and updated.<BR> |
| * At least all primary key columns must be supplied.<BR> |
| * We strongly recommend to supply the value of the update timestamp column in order to detect concurrent changes.<BR> |
| * <P> |
| * @param record the record object |
| * @param recData the record data from which to initialized the record |
| */ |
| public void initRecord(DBRecordBase record, DBRecordData recData) |
| { |
| // check param |
| checkParamRecord(record, false); |
| // Initialize the record |
| prepareInitRecord(record, false); |
| // Get Record Field Values |
| Object[] fields = record.getFields(); |
| DBColumn[] keyColumns =(DBColumn[])getKeyColumns(); |
| for (int i = 0; i < fields.length; i++) |
| { // Read a value |
| DBColumnExpr column = getColumnExprAt(i); |
| int rdi = recData.getFieldIndex(column); |
| if (rdi<0) |
| { // Field not available in Record Data |
| if (keyColumns!=null && ObjectUtils.contains(keyColumns, column)) |
| { // Error: Primary Key not supplied |
| throw new ItemNotFoundException(column.getName()); |
| } |
| if (timestampColumn == column) |
| { // Check the update Time Stamp |
| if (log.isInfoEnabled()) |
| log.info(getName() + "No record timestamp value has been provided. Hence concurrent changes will not be detected."); |
| } |
| // Set to NO_VALUE |
| fields[i] = ObjectUtils.NO_VALUE; |
| } |
| else |
| { // Copy field value (do not set to modified!) |
| fields[i] = recData.getValue(rdi); |
| } |
| } |
| // Done |
| completeInitRecord(record); |
| } |
| |
| /** |
| * initializes the Record Default Values |
| * @param record the record |
| * @param conn (optional) to allow the dbms handle autogenerated fields |
| */ |
| protected void initRecordDefaultValues(DBRecordBase record, FieldInitMode fieldInitMode) |
| { |
| // check field init mode |
| if (fieldInitMode==FieldInitMode.NONE) |
| throw new InvalidArgumentException("fieldInitMode", fieldInitMode); |
| /** |
| * Overridden in DBTable |
| * |
| * Set to NO_VALUE for Views and Queries |
| */ |
| Object[] fields = record.getFields(); |
| // Set Default values |
| for (int i = 0; i < fields.length; i++) |
| { // already set ? |
| if (fields[i]!=null) |
| continue; |
| // Set to NO_VALUE |
| fields[i] = ObjectUtils.NO_VALUE; |
| } |
| } |
| |
| /** |
| * Initialize this DBRowSet object and sets it's initial state. |
| * |
| * @param record the DBRecord object to initialize this DBRowSet object |
| * @param rowSetData any further RowSet specific data |
| * @param insert |
| */ |
| protected void prepareInitRecord(DBRecordBase record, boolean newRecord) |
| { |
| if (record==null || record.getRowSet()!=this) |
| throw new InvalidArgumentException("rec", record); |
| if (columns.size() < 1) |
| throw new ObjectNotValidException(this); |
| // Init |
| record.initData(newRecord); |
| } |
| |
| /** |
| * Completes the record initialization.<BR> |
| * Override this function to do post initialization processing. |
| * <P> |
| * @param record the DBRecord object to initialize |
| */ |
| protected void completeInitRecord(DBRecordBase record) |
| { |
| record.onRecordChanged(); |
| } |
| |
| /** |
| * Set the constraints for a single record from a supplied key |
| * @param cmd the command to which to add the constraints |
| * @param key the record key |
| */ |
| protected DBCompareExpr getKeyConstraints(Object[] key) |
| { |
| // Check Primary key |
| DBColumn[] keyColumns =(DBColumn[])getKeyColumns(); |
| if (keyColumns==null || keyColumns.length==0) |
| throw new NoPrimaryKeyException(this); // Invalid Argument |
| // Check Columns |
| if (key == null || key.length != keyColumns.length) |
| throw new InvalidKeyException(this, key); // Invalid Argument |
| // Add the key constraints |
| DBCompareExpr compareExpr = keyColumns[0].is(key[0]); |
| for (int i = 1; i < key.length; i++) |
| { // prepare key value |
| DBColumn column = keyColumns[i]; |
| Object value = key[i]; |
| // set key column constraint |
| compareExpr = compareExpr.and(column.is(value)); |
| } |
| return compareExpr; |
| } |
| |
| /** |
| * Reads a single record from the database using the given command object.<BR> |
| * If a record is found the DBRecord object will hold all record data. |
| * <P> |
| * @param record the DBRecord object which holds the record data |
| * @param cmd the SQL-Command used to query the record |
| * @param rowSetData optional rowset specific data to be held on the record |
| */ |
| protected void readRecord(DBRecordBase record, DBCommand cmd) |
| { |
| // check param |
| checkParamRecord(record, false); |
| // read now |
| DBReader reader = null; |
| try |
| { // read record using a DBReader |
| reader = new DBReader(record.getContext(), false); |
| reader.getRecordData(cmd); |
| initRecord(record, reader); |
| } catch (QueryNoResultException e) { |
| // extract key from command |
| Object key[] = cmd.getParamValues(); |
| if (key==null) |
| { // extract key from where clause |
| List<DBCompareExpr> where = cmd.getWhereConstraints(); |
| key = new Object[where.size()]; |
| int i=0; |
| for (DBCompareExpr expr : where) |
| { // get value of compare expr |
| if (expr instanceof DBCompareColExpr) |
| key[i] = ((DBCompareColExpr)expr).getValue(); |
| else |
| key[i] = "?"; // unknown |
| i++; |
| } |
| } |
| log.warn("Record [{}] not found in {}", StringUtils.toString(key, cmd.getSelect()), getName()); |
| // throw RecordNotFoundException |
| throw new RecordNotFoundException(this, key); |
| } finally { |
| reader.close(); |
| } |
| } |
| |
| /** |
| * Reads the record with the given primary key from the database. |
| * If the record cannot be found, a RecordNotFoundException is thrown. |
| * <P> |
| * @param record the DBRecord object which will hold the record data |
| * @param key the primary key values |
| public <R extends DBRecordBase> void readRecord(R record, Object[] key) |
| { |
| // Check Arguments |
| checkParamNull("key", key); |
| // Select |
| DBCommand cmd = createRecordCommand(record.getContext()); |
| cmd.select(columns); |
| // Set key constraints |
| setKeyConstraints(cmd, key); |
| // Read Record |
| readRecord(record, cmd); |
| } |
| */ |
| |
| /** |
| * Reads a record from the database |
| * @param key an array of the primary key values |
| */ |
| public void readRecord(DBRecordBase record, DBCompareExpr whereConstraints) |
| { |
| // Check Arguments |
| checkParamNull("whereConstraints", whereConstraints); |
| // check constraints |
| Set<DBColumn> columns = new HashSet<DBColumn>(); |
| whereConstraints.addReferencedColumns(columns); |
| for (DBColumn c : columns) |
| if (c.getRowSet().equals(this)==false) |
| throw new InvalidArgumentException("whereConstraints", c.getFullName()); |
| // read now |
| DBCommand cmd = createRecordCommand(record.getContext()); |
| cmd.select(getColumns()); |
| cmd.where(whereConstraints); |
| readRecord(record, cmd); |
| } |
| |
| /** |
| * Reads the partial record for a given primary key from the database |
| * @param record the DBRecord object which will hold the record data |
| * @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 void readRecord(DBRecordBase record, DBCompareExpr whereConstraints, PartialMode mode, DBColumn... columns) |
| { |
| // Check Arguments |
| checkParamNull("whereConstraints", whereConstraints); |
| // create command |
| DBCommand cmd = createRecordCommand(record.getContext()); |
| for (DBColumn column : this.columns) |
| { // key column? |
| if (isKeyColumn(column)) |
| { // always select key column |
| cmd.select(column); |
| continue; |
| } |
| // find in column list |
| for (int i=0; i<columns.length; i++) |
| { // compare column |
| if (column.equals(columns[i])) |
| { // found: add for INCLUDE |
| if (mode==PartialMode.INCLUDE) |
| cmd.select(column); |
| } |
| else if (mode==PartialMode.EXCLUDE) |
| { // not found: add for EXCLUDE |
| cmd.select(column); |
| } |
| } |
| } |
| // Set key constraints |
| cmd.where(whereConstraints); |
| // Read Record |
| readRecord(record, cmd); |
| } |
| |
| /** |
| * Returns true if the record exists in the database or false otherwise. |
| * <P> |
| * @param key an array of the primary key columns |
| * @param context the DBContext |
| * @return true if the record exists or false otherwise |
| */ |
| public boolean recordExists(Object[] key, DBContext context) |
| { |
| // Check Arguments |
| checkParamNull("key", key); |
| checkParamNull("context", context); |
| // Select |
| DBCommand cmd = createRecordCommand(context); |
| cmd.select(count()); |
| // Set key constraints |
| cmd.where(getKeyConstraints(key)); |
| // check exits |
| return (context.getUtils().querySingleInt(cmd, 0)==1); |
| } |
| |
| /** |
| * Returns true if the record exists in the database or false otherwise. |
| * <P> |
| * @param id id of the record |
| * @param context the DBContext |
| * @return true if the record exists or false otherwise |
| */ |
| public final boolean recordExists(Object id, DBContext context) |
| { |
| return recordExists(Record.key(id), context); |
| } |
| |
| /** |
| * Updates or Inserts a record in the database.<BR> |
| * Whether an update or insert is performed depends on the record state.<BR> |
| * Only modified fields will be inserted or updated in the database.<BR> |
| * <P> |
| * If a timestamp-column is set for this RowSet then a constraint will be added in the |
| * update statement in order to detect concurrent changes.<BR> |
| * If the record has been modified by another user, an error of type |
| * DBErrors.RecordUpdateFailed will be set. |
| * <P> |
| * @param record the DBRecord object. contains all fields and the field properties |
| */ |
| public <R extends DBRecordBase> void updateRecord(R record) |
| { |
| // check updateable |
| if (isUpdateable()==false) |
| throw new NotSupportedException(this, "updateRecord"); |
| // Check Arguments |
| checkParamRecord(record, true); |
| // the connection |
| DBContext context = record.getContext(); |
| Connection conn = context.getConnection(); |
| // Get the new Timestamp |
| String name = getName(); |
| Timestamp timestamp = (timestampColumn!=null) ? context.getDbms().getUpdateTimestamp(conn) : null; |
| DBMSHandler.DBSetGenKeys setGenKey = null; |
| // Get the fields and the flags |
| Object[] fields = record.getFields(); |
| // Build SQL-Statement |
| DBCommand cmd = createRecordCommand(context); |
| String sql = null; |
| int setCount = 0; |
| // Perform action |
| DBColumn[] keyColumns =(DBColumn[])getKeyColumns(); |
| DBRecordBase.State recordState = record.getState(); |
| if (recordState==DBRecordBase.State.New) |
| { // Insert Record |
| for (int i = 0; i < columns.size(); i++) |
| { // search for the column |
| Object value = fields[i]; |
| DBTableColumn col = (DBTableColumn) columns.get(i); |
| if (timestampColumn == col) |
| { // Make sure the update timestamp column is set |
| if (timestamp!=null) |
| cmd.set(col.to(timestamp)); |
| continue; |
| } |
| boolean empty = (value==ObjectUtils.NO_VALUE || ObjectUtils.isEmpty(value)); |
| if (empty && col.isAutoGenerated()) |
| { // Check for AutoInc data type |
| if (col.getDataType()==DataType.AUTOINC && |
| db.getDbms().isSupported(DBMSFeature.SEQUENCES)==false) |
| { // Obtain value via JDBC Statement.RETURN_GENERATED_KEYS |
| setGenKey = new DBSetRecordKey(fields, i); |
| continue; |
| } |
| // get the auto-generated field value |
| fields[i] = value = col.getRecordDefaultValue(conn); |
| empty = ObjectUtils.isEmpty(value); |
| } |
| // Add the value to the command |
| if (empty==false) |
| { // *** unnecessary check removed 2.5.0 *** |
| // if (col.isAutoGenerated()==false && rec.isValidateFieldValues()) |
| // col.validate(value); |
| // Insert a field |
| cmd.set(col.to(value)); |
| setCount++; |
| } |
| else if (ObjectUtils.contains(keyColumns, col)) |
| { // All primary key fields must be supplied |
| throw new FieldNotNullException(col); |
| } |
| else if (col.isRequired()) |
| { // Error Column is required! |
| throw new FieldNotNullException(col); |
| } |
| } |
| sql = cmd.getInsert(); |
| } |
| else if (recordState==DBRecordBase.State.Modified) |
| { // Update Record |
| if (keyColumns == null) |
| { // Requires a primary key |
| log.error("updateRecord: " + name + " no primary key defined!"); |
| throw new NoPrimaryKeyException(this); |
| } |
| for (int i = 0; i < columns.size(); i++) |
| { // search for the column |
| Object value = fields[i]; |
| // check for NO_VALUE |
| if (value==ObjectUtils.NO_VALUE) |
| { // Timestamp? |
| if (timestampColumn == columns.get(i)) |
| log.info("Record has no value for timestamp column. Concurrent changes will not be detected."); |
| // next |
| continue; |
| } |
| boolean modified = record.wasModified(i); |
| boolean empty = ObjectUtils.isEmpty(value); |
| DBTableColumn col = (DBTableColumn) columns.get(i); |
| if (ObjectUtils.contains(keyColumns, col)) |
| { // Check for Modification |
| if (modified == true) |
| { // Requires a primary key |
| log.warn("updateRecord: " + name + " primary has been modified!"); |
| } |
| // set pk constraint |
| cmd.where(col.is(value)); |
| } |
| else if (timestampColumn == col) |
| { // Check the update-timestamp |
| if (empty==false) |
| { // set timestamp constraint |
| cmd.where(col.is(value)); |
| } |
| // set new timestamp |
| if (timestamp!=null) |
| cmd.set(col.to(timestamp)); |
| } |
| else if (modified) |
| { // Update a field |
| if (col.isReadOnly()) |
| log.warn("updateRecord: Read-only column '" + col.getName() + " has been modified!"); |
| // *** unnecessary check removed 2.5.0 *** |
| // col.validate(value); |
| // Set the column |
| cmd.set(col.to(value)); |
| setCount++; |
| } |
| } |
| // Get the SQL statement |
| sql = cmd.getUpdate(); |
| } |
| else |
| { // Not modified |
| log.info("updateRecord: " + name + " record has not been modified! "); |
| return; |
| } |
| if (setCount == 0) |
| { // Nothing to update |
| log.info("updateRecord: " + name + " nothing to update or insert!"); |
| return; |
| } |
| // Perform action |
| DBUtils utils = context.getUtils(); |
| int affected = utils.executeSQL(sql, cmd.getParamValues(), setGenKey); |
| if (affected < 0) |
| { // Update Failed |
| throw new UnexpectedReturnValueException(affected, "db.executeSQL()"); |
| } |
| else if (affected == 0) |
| { // Record not found |
| throw new RecordUpdateFailedException(this, record.getKey()); |
| } |
| else if (affected > 1) |
| { // Multiple Records affected |
| throw new RecordUpdateAmbiguousException(this, record.getKey()); |
| } |
| // Correct Timestamp |
| if (timestampColumn!=null && timestamp!=null) |
| { // Set the correct Timestamp |
| int i = record.getFieldIndex(timestampColumn); |
| if (i >= 0) |
| fields[i] = timestamp; |
| } |
| // Change State |
| record.updateComplete(); |
| } |
| |
| /** |
| * Deletes a single record from the database.<BR> |
| * <P> |
| * @param id the record's primary key |
| * @param context the DBContext |
| */ |
| public final void deleteRecord(long id, DBContext context) |
| { |
| deleteRecord(new Object[] { id }, context); |
| } |
| |
| /** |
| * Deletes all records which reference this table. |
| * <P> |
| * @param key the key the record to be deleted |
| * @param context the DBContext |
| */ |
| protected final void deleteAllReferences(Object[] key, DBContext context) |
| { |
| // Merge Sub-Records |
| List<DBRelation> relations = db.getRelations(); |
| DBColumn[] keyColumns =(DBColumn[])getKeyColumns(); |
| if (keyColumns==null) |
| return; // No primary key - no references! |
| // Find all relations |
| for (DBRelation rel : relations) |
| { // Check cascade |
| if (rel.getOnDeleteAction()!=DBCascadeAction.CASCADE_RECORDS) |
| continue; |
| // References |
| DBReference[] refs = rel.getReferences(); |
| for (int i=0; i<refs.length; i++) |
| { |
| if (refs[i].getTargetColumn().equals(keyColumns[0])) |
| { // Found a reference on RowSet |
| DBRowSet rs = refs[0].getSourceColumn().getRowSet(); |
| rs.deleteReferenceRecords(refs, key, context); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Deletes all records which are referenced by a particular relation. |
| * <P> |
| * @param refs the reference columns belonging to the relation |
| * @param parentKey the key of the parent element |
| * @param context the DBContext |
| */ |
| protected void deleteReferenceRecords(DBReference[] refs, Object[] parentKey, DBContext context) |
| { |
| // Key length and reference length must match |
| if (refs.length!=parentKey.length) |
| throw new InvalidArgumentException("refs", refs); |
| // Rowset |
| DBColumn[] keyColumns =(DBColumn[])getKeyColumns(); |
| if (keyColumns==null || keyColumns.length==0) |
| { // No Primary Key |
| DBCommand cmd = createRecordCommand(context); |
| for (int i=0; i<parentKey.length; i++) |
| cmd.where(refs[i].getSourceColumn().is(parentKey[i])); |
| if (context.executeDelete((DBTable)this, cmd)<0) |
| throw new UnexpectedReturnValueException(-1, "db.executeSQL()"); |
| } |
| else |
| { // Query all key |
| DBCommand cmd = createRecordCommand(context); |
| cmd.select(keyColumns); |
| // Set constraints |
| for (int i=0; i<parentKey.length; i++) |
| { |
| cmd.where(refs[i].getSourceColumn().is(parentKey[i])); |
| } |
| // Set order (descending) |
| for (int i=0; i<keyColumns.length; i++) |
| { |
| cmd.orderBy(keyColumns[i], true); |
| } |
| // Query all key |
| List<Object[]> recKeys = context.getUtils().queryObjectList(cmd); |
| for (Object[] recKey : recKeys) |
| { |
| log.info("Deleting Record " + StringUtils.valueOf(recKey) + " from table " + getName()); |
| deleteRecord(recKey, context); |
| } |
| } |
| // Done |
| } |
| |
| /** |
| * Mabe use Prepared statements even if disabled in context |
| */ |
| protected DBCommand createRecordCommand(DBContext context) |
| { |
| /** |
| * alternative is: |
| * return context.getDbms().createCommand(true); |
| */ |
| return context.createCommand(); |
| } |
| |
| /** |
| * Returns additional data stored on a record by the RowSet |
| * @param record the record |
| * @return the rowset data |
| */ |
| protected final Object getRowsetData(DBRecordBase record) |
| { |
| return record.rowsetData; |
| } |
| |
| /** |
| * May be used by a Rowset to store additional data on a record |
| * @param rec the record |
| * @return the rowset data |
| */ |
| protected final void setRowsetData(DBRecordBase record, Object rowsetData) |
| { |
| record.rowsetData = rowsetData; |
| } |
| |
| } |
| |