nz.co.gregs.dbvolution.DBRow Maven / Gradle / Ivy
package nz.co.gregs.dbvolution;
import nz.co.gregs.dbvolution.databases.DBDatabase;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import nz.co.gregs.dbvolution.actions.DBQueryable;
import nz.co.gregs.dbvolution.annotations.*;
import nz.co.gregs.dbvolution.columns.AbstractColumn;
import nz.co.gregs.dbvolution.columns.ColumnProvider;
import nz.co.gregs.dbvolution.databases.definitions.DBDefinition;
import nz.co.gregs.dbvolution.datatypes.*;
import nz.co.gregs.dbvolution.exceptions.AccidentalCartesianJoinException;
import nz.co.gregs.dbvolution.exceptions.IncorrectRowProviderInstanceSuppliedException;
import nz.co.gregs.dbvolution.exceptions.UnableToInterpolateReferencedColumnInMultiColumnPrimaryKeyException;
import nz.co.gregs.dbvolution.exceptions.UnableToInstantiateDBRowSubclassException;
import nz.co.gregs.dbvolution.exceptions.UnacceptableClassForAutoFillAnnotation;
import nz.co.gregs.dbvolution.exceptions.UndefinedPrimaryKeyException;
import nz.co.gregs.dbvolution.expressions.BooleanExpression;
import nz.co.gregs.dbvolution.expressions.DBExpression;
import nz.co.gregs.dbvolution.internal.properties.*;
import nz.co.gregs.dbvolution.operators.DBOperator;
import nz.co.gregs.dbvolution.query.RowDefinition;
import org.reflections.Reflections;
/**
* DBRow is the representation of a table and its structure.
*
*
* A fuller description of creating a DBRow subclass is at the DBvolution
* website
*
* A fundamental difference between Object Oriented Programming and Relational
* Databases is that DBs are based on persistent tables that store information.
* OOP however generates a process that creates transient information.
*
*
* In Java the only persistent concept is a class, so DBvolution represents each
* table as a class. Each column of the table is represented as a Java field.
* Connecting the class and fields to the table and columns are annotations:
* {@link DBTableName} & {@link DBColumn}
*
*
* Java and Relational datatypes differ considerably, not the least because each
* DB has it's own unique version of the standard datatypes. Also DB datatypes
* are much more weakly typed. DBvolution uses
* {@link QueryableDatatype QueryableDatatypes (QDTs)} to bridge the gap between
* Java and Relational. The most common QDTs are:
* {@link DBString}, {@link DBInteger}, {@link DBDate}, and {@link DBByteArray}.
*
*
* Relational databases deliberately eschew hierarchies and has very weak links
* between entities with enormous number of entities. Conversely Java is filled
* with hierarchies and strong links while having a, relatively, tiny number of
* entities. These incompatibilities can only be resolved by avoiding a
* hierarchy, keeping the links relatively weak, and only retrieving the
* entities that have been requested to keep the number of entities relatively
* low.
*
*
* Hierarchy is avoided by not directly using any DBRow subclass within a DBRow
* subclass. Extending a DBRow subclass has it's uses but there is still little
* connection between the classes.
*
* Weak linking is achieved using the {@link DBForeignKey} annotation with the
* class of the related class. Foreign keys connect to the primary key
* (see the {@link DBPrimaryKey} annotation) of the other class to allow NATURAL
* JOINS but don't connect the classes directly and can be ignored using
* {@link DBRow#ignoreForeignKey(java.lang.Object)}.
*
* Reducing the number of entities is facilitated by making it easy for you to
* specify the tables required and add conditions to the queries. Refer to
* {@link DBDatabase}, {@link DBQuery}, {@link QueryableDatatype}, and
* {@link DBExpression} for more on this aspect of DBvolution.
*
*
* A simple DBRow subclass is shown below as an example. Please note that
* DBvolution uses a lot of reflection and thus fields must be accessible and
* there must be a public default constructor.
*
*
* @DBTableName("car_company")
* public class CarCompany extends DBRow {
*
* @DBColumn("name")
* public DBString name = new DBString();
*
* @DBPrimaryKey
* @DBColumn("uid_carcompany")
* public DBInteger uidCarCompany = new DBInteger();
*
* @DBForeignKey(Staff.class)
* @DBColumn("fk_staff_ceo")
* public DBInteger fkCEO = new DBInteger();
*
* public CarCompany() {}
* }
*
*
*
Support DBvolution at
* Patreon
*
* @author Gregory Graham
*/
abstract public class DBRow extends RowDefinition implements Serializable {
private static final long serialVersionUID = 1L;
private boolean isDefined = false;
private final List ignoredForeignKeys = Collections.synchronizedList(new ArrayList());
private transient Boolean hasBlobs;
private transient final List fkFields = new ArrayList<>();
private transient final List blobColumns = new ArrayList<>();
private transient final SortedSet> referencedTables = new TreeSet<>(new DBRow.ClassNameComparator());
private Boolean emptyRow = true;
/**
* Creates a new blank DBRow of the supplied subclass.
*
* @param DBRow type
* @param requiredDBRowClass requiredDBRowClass
* Support DBvolution at
* Patreon
* @return a new blank version of the specified class
*/
public static T getDBRow(Class requiredDBRowClass) throws UnableToInstantiateDBRowSubclassException {
try {
return requiredDBRowClass.getConstructor().newInstance();
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new UnableToInstantiateDBRowSubclassException(requiredDBRowClass, ex);
}
}
/**
* Returns a new example of the sourceRow with only the primary key set for
* use in a query.
*
* @param DBRow type
* @param sourceRow sourceRow
* Support DBvolution at
* Patreon
* @return Returns a new DBRow example, of the same class as the supplied row,
* with the same primary key as the source key
*/
public static R getPrimaryKeyExample(R sourceRow) {
@SuppressWarnings("unchecked")
R newRow = (R) getDBRow(sourceRow.getClass());
final List wrappers = sourceRow.getPrimaryKeyPropertyWrappers();
for (PropertyWrapper wrapper : wrappers) {
PropertyWrapperDefinition definition = wrapper.getPropertyWrapperDefinition();
QueryableDatatype> sourceQDT = definition.getQueryableDatatype(sourceRow);
QueryableDatatype> newQDT = definition.getQueryableDatatype(newRow);
new InternalQueryableDatatypeProxy(newQDT).setValue(sourceQDT);
}
return newRow;
}
/**
* Creates a new instance of the supplied DBRow subclass and duplicates it's
* values.
*
* @param DBRow type
* @param originalRow originalRow
* Support DBvolution at
* Patreon
* @return a new version of the specified row with values that duplicate the
* original.
*/
public static T copyDBRow(T originalRow) {
@SuppressWarnings("unchecked")
T newRow = (T) DBRow.getDBRow(originalRow.getClass());
if (originalRow.getDefined()) {
newRow.setDefined();
} else {
newRow.setUndefined();
}
for (PropertyWrapperDefinition defn : originalRow.getIgnoredForeignKeys()) {
newRow.getIgnoredForeignKeys().add(defn);
}
if (originalRow.getReturnColumns() != null) {
newRow.setReturnColumns(new ArrayList());
for (PropertyWrapperDefinition defn : originalRow.getReturnColumns()) {
newRow.getReturnColumns().add(defn);
}
} else {
newRow.setReturnColumns(null);
}
List subclassFields = originalRow.getColumnPropertyWrappers();
for (PropertyWrapper field : subclassFields) {
try {
Object originalValue = field.rawJavaValue();
if (originalValue instanceof QueryableDatatype) {
QueryableDatatype> originalQDT = (QueryableDatatype) originalValue;
field.getPropertyWrapperDefinition().setRawJavaValue(newRow, originalQDT.copy());
} else {
field.getPropertyWrapperDefinition().setRawJavaValue(newRow, originalValue);
}
} catch (IllegalArgumentException ex) {
throw new RuntimeException(ex);
}
}
return newRow;
}
private String recursiveTableAlias = null;
private transient final Object locker = new Object();
/**
* Returns the QueryableDatatype instance of the Primary Key of This DBRow
*
*
* If the DBRow class has a {@link DBPrimaryKey @DBPrimaryKey} designated
* field, then the QueryableDatatype instance of that field is returned.
*
*
Support DBvolution at
* Patreon
*
* @return the QDT of the primary key or null if there is no primary key.
*/
public List> getPrimaryKeys() {
List primaryKeyPropertyWrappers = getPrimaryKeyPropertyWrappers();
if (primaryKeyPropertyWrappers == null) {
return null;
} else {
List> names = new ArrayList<>();
for (PropertyWrapper pk : primaryKeyPropertyWrappers) {
names.add(pk.getQueryableDatatype());
}
return names;
}
}
/**
* Returns the QueryableDatatype instance of the Primary Key of This DBRow, as
* an array in case there a
*
*
* If the DBRow class has a {@link DBPrimaryKey @DBPrimaryKey} designated
* field, then the QueryableDatatype instance of that field is returned.
*
*
Support DBvolution at
* Patreon
*
* @return the QDT of the primary key or an empty array if there is no primary
* key.
*/
public QueryableDatatype>[] getPrimaryKeysAsArray() {
return getPrimaryKeys().toArray(new QueryableDatatype>[]{});
}
/**
* Set the value of the Primary Key field/column in this DBRow.
*
*
* Retrieves the PK field and calls the appropriate setValue method of the QDT
* to set the value.
*
*
* This method is dangerous as it doesn't enforce type-safety until runtime.
* However its utility is too great to ignore.
*
* @param newPKValue newPKValue
* @see DBPrimaryKey
* @see DBAutoIncrement
*/
public void setPrimaryKey(Object newPKValue) throws ClassCastException {
final List> primaryKeys = getPrimaryKeys();
if (primaryKeys == null || primaryKeys.isEmpty()) {
throw new UndefinedPrimaryKeyException(this);
} else if (primaryKeys.size() == 1) {
InternalQueryableDatatypeProxy proxy = new InternalQueryableDatatypeProxy(primaryKeys.get(0));
proxy.setValue(newPKValue);
} else {
throw new UnableToInterpolateReferencedColumnInMultiColumnPrimaryKeyException(this, primaryKeys);
}
}
/**
* Returns the field index of the Primary Key of This DBRow
*
*
* If the DBRow class has a {@link DBPrimaryKey @DBPrimaryKey} designated
* field, then the field index of that field is returned.
*
*
Support DBvolution at
* Patreon
*
* @return the index of the primary key or null if there is no primary key.
*/
public List getPrimaryKeyIndexes() {
final List primaryKeyPropertyWrappers = getPrimaryKeyPropertyWrappers();
if (primaryKeyPropertyWrappers == null) {
return null;
} else {
List names = new ArrayList<>();
for (PropertyWrapper pk : primaryKeyPropertyWrappers) {
names.add(pk.getPropertyWrapperDefinition().getColumnIndex());
}
return names;
}
}
/**
*
* Indicates whether this instance is a defined row within the database.
*
* Example objects and blank rows from an optional table are "undefined".
*
* Support DBvolution at
* Patreon
*
* @return TRUE if this row exists within the database, otherwise FALSE.
*/
public boolean getDefined() {
return isDefined;
}
/**
* indicates the DBRow is not defined in the database.
*
*
* Used internally, probably not the method you want.
*
*
*/
public void setUndefined() {
isDefined = false;
}
/**
* indicates the DBRow is defined in the database.
*
*
* Used internally, probably not the method you want.
*
*
*/
public void setDefined() {
isDefined = true;
}
/**
* Returns true if any of the non-LargeObject fields has been changed.
*
* Support DBvolution at
* Patreon
*
* @return TRUE if the simple types have changed, otherwise FALSE
*/
public boolean hasChangedSimpleTypes() {
List propertyWrappers = getWrapper().getColumnPropertyWrappers();
for (PropertyWrapper prop : propertyWrappers) {
if (!(prop.getQueryableDatatype() instanceof DBLargeObject)) {
if (prop.getQueryableDatatype().hasChanged()) {
return true;
}
}
}
return false;
}
/**
* Sets all simple types to unchanged.
*
*
* Clears the changed flag on all the simple types. {@link DBLargeObject}
* objects are not affected.
*
*
* After this method is called {@link #hasChangedSimpleTypes() } will return
* false.
*
*/
public void setSimpleTypesToUnchanged() {
List propertyWrappers = getWrapper().getColumnPropertyWrappers();
for (PropertyWrapper prop : propertyWrappers) {
final QueryableDatatype> qdt = prop.getQueryableDatatype();
if (!(qdt instanceof DBLargeObject)) {
if (qdt.hasChanged()) {
qdt.setUnchanged();
// ensure field set when using type adaptors
prop.setQueryableDatatype(qdt);
}
}
}
}
/**
* Finds the Primary Key, if there is one, and returns its column name
*
* Support DBvolution at
* Patreon
*
* @return the column of the primary key
*/
public List getPrimaryKeyColumnNames() {
List primaryKeyPropertyWrappers = getPrimaryKeyPropertyWrappers();
if (primaryKeyPropertyWrappers == null) {
return null;
} else {
List names = new ArrayList<>();
for (PropertyWrapper pk : primaryKeyPropertyWrappers) {
names.add(pk.columnName());
}
return names;
}
}
/**
* Finds the Primary Key, if there is one, and returns the name of the field.
*
* Support DBvolution at
* Patreon
*
* @return the java field name of the primary key
*/
public List getPrimaryKeyFieldName() {
List primaryKeyPropertyWrappers = getPrimaryKeyPropertyWrappers();
if (primaryKeyPropertyWrappers == null) {
return null;
} else {
List names = new ArrayList<>();
for (PropertyWrapper pk : primaryKeyPropertyWrappers) {
names.add(pk.javaName());
}
return names;
}
}
/**
* Returns the PropertyWrapper for the DBRow's primary key.
*
* Support DBvolution at
* Patreon
*
* @return the PropertyWrapper for the primary key.
*/
public List getPrimaryKeyPropertyWrappers() {
return getWrapper().getPrimaryKeysPropertyWrappers();
}
/**
* Change all the criteria specified on this DBRow instance into a list of
* strings for adding in to the WHERE clause
*
*
* Uses table and column aliases appropriate for SELECT queries
*
* @param db The DBDatabase instance that this query is to be executed on.
*
Support DBvolution at
* Patreon
* @return the WHERE clause that will be used with the current parameters
*
*/
public List getWhereClausesWithoutAliases(DBDefinition db) {
return getWhereClauses(db, false);
}
/**
* Change all the criteria specified on this DBRow instance into a list of
* strings for adding in to the WHERE clause
*
*
* Uses plain table and column names appropriate for DELETE queries
*
* @param db The DBDatabase instance that this query is to be executed on.
*
Support DBvolution at
* Patreon
* @return the WHERE clause that will be used with the current parameters
*
*/
public List getWhereClausesWithAliases(DBDefinition db) {
return getWhereClauses(db, true);
}
private List getWhereClauses(DBDefinition db, boolean useTableAlias) //throws InstantiationException, IllegalAccessException
{
List whereClause = new ArrayList<>();
List props = getWrapper().getColumnPropertyWrappers();
for (PropertyWrapper prop : props) {
if (prop.isColumn()) {
QueryableDatatype> qdt = prop.getQueryableDatatype();
String possibleWhereClause;
ColumnProvider column;
if (prop.isTypeAdapted()) {
Object rawJavaValue = prop.rawJavaValue();
if (rawJavaValue == null) {
try {
rawJavaValue = prop.getRawJavaType().newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
// note: InstantiationException tends to be thrown without a message
throw new RuntimeException("Unable to instantiate instance of " + prop.toString(), ex);
}
prop.setRawJavaValue(rawJavaValue);
}
column = this.column(rawJavaValue);
} else {
column = this.column(qdt);
}
column.setUseTableAlias(useTableAlias);
possibleWhereClause = getQDTWhereClause(db, column, qdt);
if (!possibleWhereClause.replaceAll(" ", "").isEmpty()) {
whereClause.add("(" + possibleWhereClause + ")");
}
}
}
return whereClause;
}
private String getQDTWhereClause(DBDefinition db, ColumnProvider column, QueryableDatatype> qdt) {
StringBuilder whereClause = new StringBuilder();
DBOperator op = qdt.getOperator();
if (op != null) {
if (column instanceof DBExpression) {
DBExpression requiredExpression = (DBExpression) column;
if (qdt.hasColumnExpression()) {
DBExpression[] columnExpression = qdt.getColumnExpression();
String sep = "";
for (DBExpression dBExpression : columnExpression) {
whereClause.append(sep).append(op.generateWhereExpression(db, dBExpression).toSQLString(db));
sep = db.beginAndLine();
}
} else {
whereClause = new StringBuilder(op.generateWhereExpression(db, requiredExpression).toSQLString(db));
}
}
}
return whereClause.toString();
}
/**
* USED INTERNALLY
*
* @param db
* @param useTableAlias
* Support DBvolution at
* Patreon
* @return a list of the DBExpressions that are defined for this exemplar.
*/
public final List getWhereClauseExpressions(DBDefinition db, boolean useTableAlias) //throws InstantiationException, IllegalAccessException
{
List whereClause = new ArrayList<>();
List props = getWrapper().getColumnPropertyWrappers();
for (PropertyWrapper prop : props) {
if (prop.isColumn()) {
QueryableDatatype> qdt = prop.getQueryableDatatype();
ColumnProvider column;
if (prop.isTypeAdapted()) {
Object rawJavaValue = prop.rawJavaValue();
if (rawJavaValue == null) {
try {
rawJavaValue = prop.getRawJavaType().newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
// note: InstantiationException tends to be thrown without a message
throw new RuntimeException("Unable to instantiate instance of " + prop.toString(), ex);
}
prop.setRawJavaValue(rawJavaValue);
}
column = this.column(rawJavaValue);
} else {
column = this.column(qdt);
}
column.setUseTableAlias(useTableAlias);
BooleanExpression possibleWhereClause = getQDTWhereClauseExpression(db, column, qdt);
if (possibleWhereClause != null) {
whereClause.add(possibleWhereClause);
}
}
}
return whereClause;
}
private BooleanExpression getQDTWhereClauseExpression(DBDefinition db, ColumnProvider column, QueryableDatatype> qdt) {
BooleanExpression whereClause = null;
DBOperator op = qdt.getOperator();
if (op != null) {
if (column instanceof DBExpression) {
DBExpression requiredExpression = (DBExpression) column;
if (qdt.hasColumnExpression()) {
DBExpression[] columnExpression = qdt.getColumnExpression();
for (DBExpression dBExpression : columnExpression) {
if (whereClause == null) {
whereClause = op.generateWhereExpression(db, dBExpression);
} else {
whereClause = whereClause.and(op.generateWhereExpression(db, dBExpression));
}
}
} else {
whereClause = op.generateWhereExpression(db, requiredExpression);
}
}
}
return whereClause;
}
/**
* Tests whether this DBRow instance has any criteria ({@link DBNumber#permittedValues(java.lang.Number...)
* }, etc) set.
*
*
* The database is not accessed and this method does not protect against
* functionally blank queries.
*
* @param db db
*
Support DBvolution at
* Patreon
* @return true if this DBRow instance has no specified criteria and will
* create a blank query returning the whole table.
*
*/
public boolean willCreateBlankQuery(DBDefinition db) {
List whereClause = getWhereClausesWithoutAliases(db);
return whereClause == null || whereClause.isEmpty();
}
/**
* Probably not needed by the programmer, this is the convenience function to
* find the table name specified by {@code @DBTableName} or the class name
*
* Support DBvolution at
* Patreon
*
* @return the name of the table in the database specified to correlate with
* the specified type
*
*/
public String getTableName() {
return getWrapper().tableName();
}
public String getSelectQuery() {
return getWrapper().selectQuery();
}
public void setRecursiveTableAlias(String alias) {
recursiveTableAlias = alias;
}
/**
* Returns the alias to be used if this DBRow is being used in a recursive
* query.
*
* Support DBvolution at
* Patreon
*
* @return the recursive alias set by {@link DBRecursiveQuery} during query
* execution.
*/
public String getRecursiveTableAlias() {
return recursiveTableAlias;
}
/**
* Returns the same result as {@link #toString() } but omitting the Foreign
* Key references.
*
* Support DBvolution at
* Patreon
*
* @return a string representation of the contents of this instance with
* Foreign Key fields removed
*/
public String toStringMinusFKs() {
StringBuilder string = new StringBuilder();
List fields = getWrapper().getColumnPropertyWrappers();
String separator = "";
for (PropertyWrapper field : fields) {
if (field.isColumn()) {
if (!field.isForeignKey()) {
string.append(separator);
string.append(" ");
string.append(field.javaName());
string.append(":");
string.append(field.getQueryableDatatype());
separator = ",";
}
}
}
return string.toString();
}
/**
*
* Support DBvolution at
* Patreon
*
* @return a list of all foreign keys, MINUS the ignored foreign keys
*/
public List getForeignKeyPropertyWrappers() {
synchronized (fkFields) {
if (fkFields.isEmpty()) {
List props = getWrapper().getForeignKeyPropertyWrappers();
for (PropertyWrapper prop : props) {
if (prop.isColumn()) {
if (prop.isForeignKey()) {
if (!ignoredForeignKeys.contains(prop.getPropertyWrapperDefinition())) {
fkFields.add(prop);
}
}
}
}
}
return fkFields;
}
}
/**
* Ignores the foreign key of the property (field or method) given the
* property's object reference.
*
*
* For example the following code snippet will ignore the foreign key on the
* fkAddress field:
*
* Customer customer = ...;
* customer.ignoreForeignKey(customer.fkAddress);
*
*
*
* Requires the field to be from this instance to work.
*
* @param qdt qdt
*/
public void ignoreForeignKey(Object qdt) throws IncorrectRowProviderInstanceSuppliedException {
PropertyWrapper fkProp = getPropertyWrapperOf(qdt);
if (fkProp == null) {
throw new IncorrectRowProviderInstanceSuppliedException(this, qdt);
}
getIgnoredForeignKeys().add(fkProp.getPropertyWrapperDefinition());
fkFields.clear();
}
/**
* Ignores the foreign keys of the property (field or method) given the
* property's object reference.
*
*
* For example the following code snippet will ignore the foreign key on the
* fkAddress field:
*
* Customer customer = ...;
* customer.ignoreForeignKeys(customer.fkAddress, customer.fkManager);
*
*
*
* Requires the field to be from this instance to work.
*
* @param qdts qdts
*/
public void ignoreForeignKeys(Object... qdts) throws IncorrectRowProviderInstanceSuppliedException {
for (Object object : qdts) {
ignoreForeignKey(object);
}
}
/**
* Ignores the foreign keys of the property (field or method) given the
* property's object reference.
*
*
* For example the following code snippet will ignore the foreign key on the
* fkAddress field:
*
* Customer customer = ...;
* customer.ignoreForeignKeys(customer.fkAddress, customer.fkManager);
*
*
*
* Requires the field to be from this instance to work.
*
* @param columns columns
*/
public void ignoreForeignKeys(ColumnProvider... columns) throws IncorrectRowProviderInstanceSuppliedException {
for (ColumnProvider col : columns) {
ignoreForeignKey(col);
}
}
/**
* Ignores the foreign key of the column provided.
*
* Similar to {@link #ignoreForeignKey(java.lang.Object) } but uses a
* ColumnProvider which is portable between instances of DBRow.
*
* For example the following code snippet will ignore the foreign key provided
* by a different instance of Customer:
*
* Customer customer = ...;
* IntegerColumn addressColumn = customer.column(customer.fkAddress);
* Customer cust2 = new Customer();
* cust2.ignoreForeignKey(addressColumn);
*
*
* @param column column
*/
public void ignoreForeignKey(ColumnProvider column) {
PropertyWrapper fkProp = column.getColumn().getPropertyWrapper();
getIgnoredForeignKeys().add(fkProp.getPropertyWrapperDefinition());
fkFields.clear();
}
/**
* Removes all foreign keys from the "ignore" list.
*
* @see DBRow#ignoreAllForeignKeys()
* @see DBRow#ignoreAllForeignKeysExceptFKsTo(nz.co.gregs.dbvolution.DBRow...)
* @see DBRow#ignoreForeignKey(java.lang.Object)
* @see DBRow#ignoreForeignKey(nz.co.gregs.dbvolution.columns.ColumnProvider)
*
*/
public void useAllForeignKeys() {
getIgnoredForeignKeys().clear();
fkFields.clear();
}
/**
* Adds All foreign keys to the "ignore" list.
*
* All foreign keys of this instance will be ignored. This may cause an
* {@link AccidentalCartesianJoinException} if no additional relationships
* have been added.
*
*/
public void ignoreAllForeignKeys() {
List props = this.getForeignKeyPropertyWrappers();
for (PropertyWrapper prop : props) {
getIgnoredForeignKeys().add(prop.getPropertyWrapperDefinition());
}
fkFields.clear();
}
/**
* Adds All foreign keys to the "ignore" list except those specified.
*
* All foreign keys of this instance will be ignored. This may cause an
* {@link AccidentalCartesianJoinException} if no additional relationships
* have been added.
*
* @param importantForeignKeys importantForeignKeys
*/
public void ignoreAllForeignKeysExcept(Object... importantForeignKeys) throws IncorrectRowProviderInstanceSuppliedException {
ArrayList importantFKs = new ArrayList<>();
for (Object object : importantForeignKeys) {
PropertyWrapper importantProp = getPropertyWrapperOf(object);
if (importantProp != null) {
if (importantProp.isColumn() && importantProp.isForeignKey()) {
importantFKs.add(importantProp.getPropertyWrapperDefinition());
}
} else {
throw new IncorrectRowProviderInstanceSuppliedException(this, object);
}
}
List props = this.getForeignKeyPropertyWrappers();
for (PropertyWrapper prop : props) {
final PropertyWrapperDefinition propDefn = prop.getPropertyWrapperDefinition();
if (!importantFKs.contains(propDefn)) {
getIgnoredForeignKeys().add(propDefn);
}
}
fkFields.clear();
}
/**
* Indicates if the DBRow has {@link DBLargeObject} (BLOB) columns.
*
* If the DBrow has columns that represent BLOB, CLOB, TEXT, JAVA_OBJECT, or
* other large object columns, this method with indicate it.
*
* Support DBvolution at
* Patreon
*
* @return TRUE if this DBRow has large object columns, FALSE otherwise.
*/
public boolean hasLargeObjects() {
synchronized (blobColumns) {
if (hasBlobs == null) {
hasBlobs = Boolean.FALSE;
for (PropertyWrapper prop : getColumnPropertyWrappers()) {
if (prop.isInstanceOfLargeObject()) {
blobColumns.add(prop);
hasBlobs = Boolean.TRUE;
}
}
}
return hasBlobs;
}
}
/**
* Removes all fields of this DBRow from the query results.
*
*
* All fields will be removed and the returned rows will be effectively a NULL
* row, however the DBRow's table will still be used in the query to set
* conditions.
*
*/
public final void setReturnFieldsToNone() {
setReturnColumns(new ArrayList());
}
/**
* Limits the returned columns by the specified properties (fields and/or
* methods) given the properties object references.
*
*
* For example the following code snippet will include only the uid and name
* columns based on the uid and name fields:
*
* Customer customer = ...;
* customer.setReturnFields(customer.uid, customer.name);
*
*
*
* Requires the field to be from this instance to work.
*
* @param fields a list of fields/methods from this object
*/
public final void setReturnFields(Object... fields) throws IncorrectRowProviderInstanceSuppliedException {
setReturnColumns(new ArrayList());
PropertyWrapper propWrapper;
for (Object property : fields) {
propWrapper = getPropertyWrapperOf(property);
if (propWrapper == null) {
throw new IncorrectRowProviderInstanceSuppliedException(this, property);
}
getReturnColumns().add(propWrapper.getPropertyWrapperDefinition());
}
}
/**
* Limits the returned columns by the specified properties (fields and/or
* methods) given the properties object references.
*
*
* For example the following code snippet will include only the uid and name
* columns based on the uid and name fields:
*
* Customer customer = ...;
* customer.setReturnFields(customer.uid, customer.name);
*
*
*
* Requires the field to be from this instance to work.
*
* @param columns a list of fields/methods from this object
*/
public final void setReturnFields(ColumnProvider... columns) throws IncorrectRowProviderInstanceSuppliedException {
setReturnFieldsToNone();
for (ColumnProvider provider : columns) {
final AbstractColumn column = provider.getColumn();
Object appropriateFieldFromRow = column.getAppropriateFieldFromRow(this);
this.setReturnFields(appropriateFieldFromRow);
}
}
/**
* Extends the returned columns to include the specified properties (fields
* and/or methods) given the properties object references.
*
*
* For example the following code snippet will include only the uid, name, and
* address columns based on the uid and name fields:
*
* Customer customer = ...;
* customer.setReturnFields(customer.uid, customer.name);
* customer.addReturnFields(customer.address);
*
*
*
* Requires the field to be from this instance to work.
*
* @param fields a list of fields/methods from this object
*/
public final void addReturnFields(Object... fields) throws IncorrectRowProviderInstanceSuppliedException {
if (getReturnColumns() == null) {
setReturnColumns(new ArrayList());
}
PropertyWrapper propWrapper;
for (Object property : fields) {
propWrapper = getPropertyWrapperOf(property);
if (propWrapper == null) {
throw new IncorrectRowProviderInstanceSuppliedException(this, property);
}
getReturnColumns().add(propWrapper.getPropertyWrapperDefinition());
}
}
/**
* Removes all builtin columns from the return list.
*
*
* Used by DBReport to avoid returning fields that haven't been specified with
* an expression.
*
*
* Probably not useful in general use.
*
*/
protected final void removeAllFieldsFromResults() {
setReturnFields(new Object[]{});
}
/**
* Remove all limitations on the fields returned.
*
*
* Clears the limits on returned fields set by
* {@code DBRow.setReturnFields(T...)}
*
*
*/
public void returnAllFields() {
setReturnColumns(null);
}
/**
* List the foreign keys and ad-hoc relationships from this instance to the
* supplied example as DBRelationships
*
* @param otherTable otherTable
*
Support DBvolution at
* Patreon
* @return the foreign keys and ad-hoc relationships as an SQL String or a
* null pointer
*/
public List getRelationshipsAsBooleanExpressions(DBRow otherTable) {
List rels = new ArrayList<>();
List fks = getForeignKeyPropertyWrappers();
for (PropertyWrapper fk : fks) {
Class extends DBRow> referencedClass = fk.referencedClass();
if (referencedClass.isAssignableFrom(otherTable.getClass())) {
rels.add(getRelationshipExpressionFor(this, fk, otherTable));
}
}
fks = otherTable.getForeignKeyPropertyWrappers();
for (PropertyWrapper fk : fks) {
Class extends DBRow> referencedClass = fk.referencedClass();
if (referencedClass.isAssignableFrom(this.getClass())) {
rels.add(getRelationshipExpressionFor(otherTable, fk, this));
}
}
return rels;
}
/**
* Tests whether this instance of DBRow and the otherTable instance of DBRow
* will be connected given the specified database and query options.
*
* @param otherTable otherTable
* Support DBvolution at
* Patreon
* @return TRUE if this instance and the otherTable will be connected, FALSE
* otherwise.
*/
public boolean willBeConnectedTo(DBRow otherTable) {
List relationshipsAsBooleanExpressions = this.getRelationshipsAsBooleanExpressions(otherTable);
relationshipsAsBooleanExpressions.addAll(otherTable.getRelationshipsAsBooleanExpressions(this));
return (!relationshipsAsBooleanExpressions.isEmpty());
}
/**
* Returns all the DBRow subclasses referenced by this class with foreign keys
*
*
* Similar to {@link #getAllConnectedTables() } but where this class directly
* references the external DBRow subclass with an {@code @DBForeignKey}
* annotation.
*
*
* That is to say: where A is this class, returns a List of B such that A
* => B
*
*
Support DBvolution at
* Patreon
*
* @return A set of DBRow subclasses referenced with {@code @DBForeignKey}
*
*/
@SuppressWarnings("unchecked")
public SortedSet> getReferencedTables() {
synchronized (referencedTables) {
if (referencedTables.isEmpty()) {
List props = getWrapper().getForeignKeyPropertyWrappers();
for (PropertyWrapper prop : props) {
referencedTables.add(prop.referencedClass());
}
}
}
final SortedSet> returnSet = new TreeSet<>(new DBRow.ClassNameComparator());
returnSet.addAll(referencedTables);
return returnSet;
}
/**
* Returns all the DBRow subclasses referenced by this class with foreign keys
*
*
* Similar to {@link #getAllConnectedTables() } but where this class directly
* references the external DBRow subclass with an {@code @DBForeignKey}
* annotation.
*
*
* That is to say: where A is this class, returns a List of B such that A
* => B
*
*
Support DBvolution at
* Patreon
*
* @return A set of DBRow subclasses referenced with {@code @DBForeignKey}
*
*/
@SuppressWarnings("unchecked")
public SortedSet> getReferencedBaseTables() {
synchronized (referencedTables) {
if (referencedTables.isEmpty()) {
List props = getWrapper().getForeignKeyPropertyWrappers();
for (PropertyWrapper prop : props) {
final Class extends DBRow> referencedClass = prop.referencedClass();
if (referencedClass.getSuperclass().equals(DBRow.class)) {
referencedTables.add(referencedClass);
}
}
}
}
final SortedSet> returnSet = new TreeSet<>(new DBRow.ClassNameComparator());
returnSet.addAll(referencedTables);
return returnSet;
}
/**
* Creates a set of all DBRow subclasses that are connected to this class.
*
*
* Uses {@link #getReferencedTables() } and {@link #getRelatedTables() } to
* produce a complete list of tables connected by a foreign key to this DBRow
* class.
*
*
* That is to say: where A is this class, returns a List of B such that B
* => A or A => B
*
*
Support DBvolution at
* Patreon
*
* @return a set of classes that have a {@code @DBForeignKey} reference to or
* from this class
*/
public SortedSet> getAllConnectedTables() {
final SortedSet> relatedTables = new TreeSet<>(new DBRow.ClassNameComparator());
relatedTables.addAll(getRelatedTables());
relatedTables.addAll(getReferencedTables());
return relatedTables;
}
/**
* Creates a set of all DBRow direct subclasses that are connected to this
* class.
*
*
* Uses {@link #getReferencedBaseTables() } and {@link #getRelatedBaseTables()
* } to produce a complete list of base tables connected by a foreign key to
* this DBRow class.
*
*
* That is to say: where A is this class, returns a List of B such that B
* => A or A => B
*
*
* Base tables are direct subclasses of DBRow, generally this means they
* represent actual database tables and not a subset of a base table.
*
*
Support DBvolution at
* Patreon
*
* @return a set of classes that have a {@code @DBForeignKey} reference to or
* from this class
*/
public SortedSet> getAllConnectedBaseTables() {
final SortedSet> relatedTables = new TreeSet<>(new DBRow.ClassNameComparator());
relatedTables.addAll(getRelatedBaseTables());
relatedTables.addAll(getReferencedBaseTables());
return relatedTables;
}
/**
* Creates a set of all DBRow subclasses that reference this class with
* foreign keys.
*
*
* Similar to {@link #getReferencedTables() } but where this class is being
* referenced by the external DBRow subclass.
*
*
* That is to say: where A is this class, returns a List of B such that B
* => A
*
*
Support DBvolution at
* Patreon
*
* @return a set of classes that have a {@code @DBForeignKey} reference to
* this class
*/
public SortedSet> getRelatedTables() throws UnableToInstantiateDBRowSubclassException {
SortedSet> relatedTables = new TreeSet<>(new DBRow.ClassNameComparator());
Reflections reflections = new Reflections(this.getClass().getPackage().getName());
Set> subTypes = reflections.getSubTypesOf(DBRow.class);
for (Class extends DBRow> tableClass : subTypes) {
try {
if (!Modifier.isAbstract(tableClass.getModifiers())) {
DBRow newInstance = tableClass.newInstance();
if (newInstance.getReferencedTables().contains(this.getClass())) {
relatedTables.add(tableClass);
}
}
} catch (InstantiationException | IllegalAccessException ex) {
throw new UnableToInstantiateDBRowSubclassException(tableClass, ex);
}
}
return relatedTables;
}
/**
* Creates a set of all DBRow subclasses that reference this class with
* foreign keys.
*
*
* Similar to {@link #getReferencedTables() } but where this class is being
* referenced by the external DBRow subclass.
*
*
* That is to say: where A is this class, returns a List of B such that B
* => A
*
*
Support DBvolution at
* Patreon
*
* @return a set of classes that have a {@code @DBForeignKey} reference to
* this class
*/
public SortedSet> getRelatedBaseTables() throws UnableToInstantiateDBRowSubclassException {
SortedSet> relatedTables = new TreeSet<>(new DBRow.ClassNameComparator());
Reflections reflections = new Reflections(this.getClass().getPackage().getName());
Set> subTypes = reflections.getSubTypesOf(DBRow.class);
for (Class extends DBRow> tableClass : subTypes) {
if (tableClass.getSuperclass().equals(DBRow.class)) {
try {
if (!Modifier.isAbstract(tableClass.getModifiers())) {
DBRow newInstance = tableClass.newInstance();
if (newInstance.getReferencedTables().contains(this.getClass())) {
relatedTables.add(tableClass);
}
}
} catch (InstantiationException | IllegalAccessException ex) {
throw new UnableToInstantiateDBRowSubclassException(tableClass, ex);
}
}
}
return relatedTables;
}
/**
* Ignores the Foreign Keys to all tables except those to the supplied DBRows
*
* @param goodTables goodTables
*/
public void ignoreAllForeignKeysExceptFKsTo(DBRow... goodTables) {
List props = getWrapper().getForeignKeyPropertyWrappers();
for (PropertyWrapper prop : props) {
boolean ignore = true;
for (DBRow goodTable : goodTables) {
if (prop.isForeignKeyTo(goodTable)) {
ignore = false;
break;
}
}
if (ignore) {
ignoreForeignKey(prop.getQueryableDatatype());
}
}
}
/**
* Returns all fields that represent BLOB columns such DBLargeObject,
* DBByteArray, or DBJavaObject.
*
* Support DBvolution at
* Patreon
*
* @return a list of {@link QueryableDatatype} that are large objects in this
* object.
*/
public List> getLargeObjects() {
// Initialise the blob columns list if necessary
ArrayList> returnList = new ArrayList<>();
if (hasLargeObjects()) {
for (PropertyWrapper propertyWrapper : blobColumns) {
returnList.add(propertyWrapper.getQueryableDatatype());
}
}
return returnList;
}
/**
* Finds all instances of {@code example} that share a {@link DBQueryRow} with
* this instance.
*
* @param DBRow
* @param query query
* @param example example
* Support DBvolution at
* Patreon
* @return all instances of {@code example} that are connected to this
* instance in the {@code query} 1 Database exceptions may be thrown
* @throws java.sql.SQLException java.sql.SQLException
*/
public List getRelatedInstancesFromQuery(DBQueryable query, R example) throws SQLException {
List instances = new ArrayList<>();
final List allRows = query.getAllRows();
for (DBQueryRow qrow : allRows) {
DBRow versionOfThis = qrow.get(this);
R versionOfThat = qrow.get(example);
if (versionOfThis.equals(this) && versionOfThat != null) {
instances.add(versionOfThat);
}
}
return instances;
}
/**
* Indicates whether this instance has any values set from the database.
*
*
* If this row is the result of the database sending back a row with NULL in
* every column, this method returns TRUE.
*
*
* An empty row is probably the result an optional table not having a matching
* row for the query. In database parlance this row is a null row of an OUTER
* JOIN and this table did not have any matching rows.
*
*
* Only used internally as DBQuery results produce NULLs for non-existent
* rows.
*
*
* Please note: if the row is undefined
* {@link DBRow#isDefined (see isDefined)} then this is meaningless
*
*
Support DBvolution at
* Patreon
*
* @return TRUE if the row has no non-null values or is undefined, FALSE
* otherwise
*/
public Boolean isEmptyRow() {
return emptyRow;
}
/**
* Sets the row to be empty or not.
*
* Used within DBQuery while creating the DBRows to indicate an empty row.
*
* @param isThisRowEmpty isThisRowEmpty
*/
public void setEmptyRow(Boolean isThisRowEmpty) {
this.emptyRow = isThisRowEmpty;
}
public List getSelectedProperties() {
if (getReturnColumns() == null) {
return getColumnPropertyWrappers();
} else {
ArrayList selected = new ArrayList<>();
for (PropertyWrapperDefinition proDef : getReturnColumns()) {
for (PropertyWrapper pro : getColumnPropertyWrappers()) {
if (pro.getPropertyWrapperDefinition().equals(proDef)) {
selected.add(pro);
}
}
}
return selected;
}
}
boolean isPeerOf(DBRow table) {
final RowDefinitionClassWrapper thisClassWrapper = this.getWrapper().getClassWrapper();
final RowDefinitionClassWrapper thatClassWrapper = table.getWrapper().getClassWrapper();
return thisClassWrapper.equals(thatClassWrapper);
}
/**
* Finds all fields is this object that are foreign keys to the table
* represented by the supplied DBRow.
*
*
* Returned QueryableDatatypes do not necessarily reference the supplied
* DBRow.
*
*
* Values of the primary key and foreign keys are not compared.
*
* @param DBRow type
* @param row row
* Support DBvolution at
* Patreon
* @return a list of {@link QueryableDatatype} that are foreign keys to the
* {@link DBRow}
*/
public List> getForeignKeysTo(R row) {
List> fksToR = new ArrayList<>();
RowDefinitionInstanceWrapper wrapper = getWrapper();
List foreignKeyPropertyWrappers = wrapper.getForeignKeyPropertyWrappers();
for (PropertyWrapper propertyWrapper : foreignKeyPropertyWrappers) {
if (propertyWrapper.isForeignKeyTo(row)) {
fksToR.add(propertyWrapper.getQueryableDatatype());
}
}
return fksToR;
}
/**
* Provides DBExpressions representing the FK relationship between this DBRow
* and the target specified.
*
*
* Values of the primary key and foreign keys are not compared.
*
* @param DBRow type
* @param target target
* Support DBvolution at
* Patreon
* @return a list of {@link DBExpression DBExpressions} that are foreign keys
* to the {@link DBRow target}
*/
public List getForeignKeyExpressionsTo(R target) {
List fksToR = new ArrayList<>();
RowDefinitionInstanceWrapper wrapper = getWrapper();
List foreignKeyPropertyWrappers = wrapper.getForeignKeyPropertyWrappers();
for (PropertyWrapper propertyWrapper : foreignKeyPropertyWrappers) {
if (propertyWrapper.isForeignKeyTo(target)) {
RowDefinition source = propertyWrapper.getRowDefinitionInstanceWrapper().adapteeRowDefinition();
final QueryableDatatype> sourceFK = propertyWrapper.getQueryableDatatype();
PropertyWrapperDefinition targetPropertyDefinition = propertyWrapper.getPropertyWrapperDefinition().referencedPropertyDefinitionIdentity();
PropertyWrapper targetProperty = target.getWrapper().getPropertyByName(targetPropertyDefinition.javaName());
QueryableDatatype> targetPK = targetProperty.getQueryableDatatype().getQueryableDatatypeForExpressionValue();
Object column = source.column(sourceFK);
try {
final Method isMethod = column.getClass().getMethod("is", targetPK.getClass());
if (isMethod != null) {
Object fkExpression = isMethod.invoke(column, targetPK);
if (DBExpression.class.isAssignableFrom(fkExpression.getClass())) {
fksToR.add((DBExpression) fkExpression);
}
}
} catch (IllegalAccessException | IllegalArgumentException | NoSuchMethodException | SecurityException | InvocationTargetException ex) {
throw new nz.co.gregs.dbvolution.exceptions.ForeignKeyCannotBeComparedToPrimaryKey(ex, source, propertyWrapper, target, targetProperty);
}
}
}
return fksToR;
}
/**
* Support DBvolution at
* Patreon
*
* @return the ignoredForeignKeys
*/
protected List getIgnoredForeignKeys() {
return ignoredForeignKeys;
}
/**
* Ignores the foreign keys of the property (field or method) given the
* property's object reference.
*
*
* For example the following code snippet will ignore the foreign key on the
* fkAddress field:
*
* Customer customer = ...;
* fksToIgnore.add(customer.fkAddress);
* customer.ignoreForeignKeyProperties(fksToIgnore);
*
*
*
* Requires the field to be from this instance to work.
*
* @param ignoreTheseFKs ignoreTheseFKs
* @see #ignoreForeignKey(java.lang.Object)
*/
public void ignoreForeignKeyProperties(Collection