/**********************************************************************
Copyright (c) 2002 Kelly Grizzle (TJDO) and others. All rights reserved.
Licensed 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.
Contributors:
2002 Mike Martin (TJDO)
2003 Andy Jefferson - added comments
2003 Erik Bengtson - removed unused import
2003 Erik Bengtson - fixed bug [832635] Imcompatiblities in casting
inherited classes in query
2003 Andy Jefferson - coding standards
2004 Andy Jefferson - updated retrieval of ColumnLists
2004 Marco Schulze - replaced catch(NotPersistenceCapableException ...) by
advance-check via TypeManager.isSupportedType(...)
2004 Andy Jefferson - moved statements to AbstractSetStore
2005 Andy Jefferson - added dependent-element when removed from collection
...
**********************************************************************/
package org.datanucleus.store.mapped.scostore;
import org.datanucleus.ClassLoaderResolver;
import org.datanucleus.ManagedConnection;
import org.datanucleus.ObjectManager;
import org.datanucleus.StateManager;
import org.datanucleus.exceptions.NucleusDataStoreException;
import org.datanucleus.exceptions.NucleusUserException;
import org.datanucleus.metadata.AbstractClassMetaData;
import org.datanucleus.metadata.AbstractMemberMetaData;
import org.datanucleus.metadata.FieldRole;
import org.datanucleus.metadata.MetaDataUtils;
import org.datanucleus.metadata.Relation;
import org.datanucleus.sco.SCOMtoN;
import org.datanucleus.store.mapped.DatastoreClass;
import org.datanucleus.store.mapped.DatastoreContainerObject;
import org.datanucleus.store.mapped.DatastoreIdentifier;
import org.datanucleus.store.mapped.exceptions.MappedDatastoreException;
import org.datanucleus.store.mapped.expression.LogicSetExpression;
import org.datanucleus.store.mapped.expression.QueryExpression;
import org.datanucleus.store.mapped.expression.ScalarExpression;
import org.datanucleus.store.mapped.expression.UnboundVariable;
import org.datanucleus.store.mapped.mapping.JavaTypeMapping;
import org.datanucleus.store.mapped.query.IncompatibleQueryElementTypeException;
import org.datanucleus.util.ClassUtils;
import org.datanucleus.util.NucleusLogger;
import org.datanucleus.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
/**
* Representation of a JoinTable Set as part of a relationship. This class is
* used where you have a 1-N and the tables are joined via a link table.
* That is one table is the owner, and it has a link table to another table,
* with the link table having 2 columns - the ids of the 2 tables.
* This is in contrast to FKSetStore which represents 1-N relationships
* without using a link table (using an id in the other table).
* <p>
* For sets of primitive types (e.g Date,String etc), the JoinSetStore is used,
* but the 'link' table contains the id of the owner and the field(s)
* representing the primitive type.
*/
public abstract class JoinSetStore extends AbstractSetStore
{
/**
* Constructor for the relationship representation.
* @param mmd Metadata for the owner member
* @param joinTable The table for the link
* @param clr The ClassLoaderResolver
*/
public JoinSetStore(AbstractMemberMetaData mmd, DatastoreContainerObject joinTable,
ClassLoaderResolver clr, JavaTypeMapping ownerMapping, JavaTypeMapping elementMapping,
JavaTypeMapping orderMapping, JavaTypeMapping relationDiscriminatorMapping,
String relationDiscriminatorValue, boolean isEmbeddedElement, boolean isSerialisedElement,
AbstractSetStoreSpecialization specialization)
{
super(joinTable.getStoreManager(), clr, specialization);
// A Set really needs a SetTable, but we need to cope with the situation
// where a user declares a field as Collection but is instantiated as a List or a Set
// so we just accept CollectionTable and rely on it being adequate
this.containerTable = joinTable;
setOwner(mmd, clr);
this.ownerMapping = ownerMapping;
this.elementMapping = elementMapping;
this.orderMapping = orderMapping;
this.relationDiscriminatorMapping = relationDiscriminatorMapping;
this.relationDiscriminatorValue = relationDiscriminatorValue;
this.elementType = mmd.getCollection().getElementType();
this.elementsAreEmbedded = isEmbeddedElement;
this.elementsAreSerialised = isSerialisedElement;
if (elementsAreSerialised)
{
elementInfo = null;
}
else
{
Class element_class = clr.classForName(elementType);
if (ClassUtils.isReferenceType(element_class))
{
// Collection of reference types (interfaces/Objects)
String[] implNames = MetaDataUtils.getInstance().getImplementationNamesForReferenceField(
ownerMemberMetaData, FieldRole.ROLE_COLLECTION_ELEMENT, clr);
elementInfo = new ElementInfo[implNames.length];
for (int i = 0; i < implNames.length; i++)
{
DatastoreClass table = storeMgr.getDatastoreClass(implNames[i], clr);
AbstractClassMetaData cmd =
storeMgr.getOMFContext().getMetaDataManager().getMetaDataForClass(implNames[i], clr);
elementInfo[i] = new ElementInfo(cmd, table);
}
}
else
{
// Set<PC>, Set<Non-PC>
// Generate the information for the possible elements
emd = storeMgr.getOMFContext().getMetaDataManager().getMetaDataForClass(element_class, clr);
if (emd != null && !elementsAreEmbedded)
{
elementInfo = getElementInformationForClass();
}
else
{
elementInfo = null;
}
}
}
}
// -------------------- Overridden SetStore methods ------------------------
/**
* Method to update the collection to be the supplied collection of elements.
* @param sm StateManager of the object
* @param coll The collection to use
*/
public void update(StateManager sm, Collection coll)
{
if (coll == null || coll.isEmpty())
{
clear(sm);
return;
}
if (ownerMemberMetaData.getCollection().isSerializedElement() || ownerMemberMetaData.getCollection().isEmbeddedElement())
{
// Serialized/Embedded elements so just clear and add again
clear(sm);
addAll(sm, coll, 0);
return;
}
// Find existing elements, and remove any that are no longer present
Iterator elemIter = iterator(sm);
Collection existing = new HashSet();
while (elemIter.hasNext())
{
Object elem = elemIter.next();
if (!coll.contains(elem))
{
remove(sm, elem, -1, true);
}
else
{
existing.add(elem);
}
}
if (existing.size() != coll.size())
{
// Add any elements that aren't already present
Iterator iter = coll.iterator();
while (iter.hasNext())
{
Object elem = iter.next();
if (!existing.contains(elem))
{
add(sm, elem, 0);
}
}
}
}
/**
* Remove all elements from a collection from the association owner vs elements.
* @param sm State Manager for the container
* @param elements Collection of elements to remove
* @return Whether the database was updated
*/
public boolean removeAll(StateManager sm, Collection elements, int size)
{
if (elements == null || elements.size() == 0)
{
return false;
}
boolean modified = removeAllInternal(sm, elements, size);
if (ownerMemberMetaData.getCollection().isDependentElement())
{
// "delete-dependent" : delete elements if the collection is marked as dependent
// TODO What if the collection contains elements that are not in the Set ? should not delete them
sm.getObjectManager().deleteObjects(elements.toArray());
}
return modified;
}
protected abstract boolean removeAllInternal(StateManager sm, Collection elements, int size);
/**
* Convenience method to check if an element already refers to the owner in an M-N relation.
* @param ownerSM State Manager of the owner
* @param element The element
* @return Whether the element contains the owner
*/
private boolean elementAlreadyContainsOwnerInMtoN(StateManager ownerSM, Object element)
{
ObjectManager om = ownerSM.getObjectManager();
StateManager elementSM = om.findStateManager(element);
AbstractMemberMetaData[] relatedMmds = ownerMemberMetaData.getRelatedMemberMetaData(om.getClassLoaderResolver());
Object elementSCO = elementSM.provideField(relatedMmds[0].getAbsoluteFieldNumber());
if (elementSCO instanceof SCOMtoN)
{
// The field is already a SCO wrapper so just query it
if (contains(ownerSM, element))
{
NucleusLogger.DATASTORE.info(LOCALISER.msg("056040", ownerMemberMetaData.getFullFieldName(), element));
return true;
}
}
else
{
// The element is not a SCO wrapper so query the datastore directly
// TODO Fix this. It is inefficient to go off to the datastore to check whether a record exists.
// This should be changed in line with TCK test AllRelationships since that tries to load up
// many relationships, and CollectionMapping.postInsert calls addAll with these hence we don't
// have SCO's to use contains() on
if (locate(ownerSM, element))
{
NucleusLogger.DATASTORE.info(LOCALISER.msg("056040", ownerMemberMetaData.getFullFieldName(), element));
return true;
}
}
return false;
}
/**
* Method to check for the existence in the datastore of an owner-element relation.
* @param sm State Manager for the owner
* @param element The element
* @return Whether the relation exists in the datastore
*/
public abstract boolean locate(StateManager sm, Object element);
/**
* Adds one element to the association owner vs elements.
* @param sm State Manager for the container.
* @param element Element to add
* @return Whether it was successful
*/
public boolean add(StateManager sm, Object element, int size)
{
// Check that the object is valid for writing
validateElementForWriting(sm, element, null);
if (relationType == Relation.ONE_TO_MANY_BI && sm.getObjectManager().getOMFContext()
.getPersistenceConfiguration().getBooleanProperty("datanucleus.manageRelationships"))
{
// Managed Relations : make sure we have consistency of relation
StateManager elementSM = sm.getObjectManager().findStateManager(element);
if (elementSM != null)
{
AbstractMemberMetaData[] relatedMmds = ownerMemberMetaData.getRelatedMemberMetaData(clr);
Object elementOwner = elementSM.provideField(relatedMmds[0].getAbsoluteFieldNumber());
if (elementOwner == null)
{
// No owner, so correct it
NucleusLogger.JDO.info(LOCALISER.msg("056037", StringUtils.toJVMIDString(sm.getObject()), ownerMemberMetaData
.getFullFieldName(), StringUtils.toJVMIDString(elementSM.getObject())));
elementSM.replaceField(relatedMmds[0].getAbsoluteFieldNumber(), sm.getObject(), false);
}
else if (elementOwner != sm.getObject() && sm.getReferencedPC() == null)
{
// Owner of the element is neither this container nor being attached
// Inconsistent owner, so throw exception
throw new NucleusUserException(LOCALISER.msg("056038", StringUtils.toJVMIDString(sm.getObject()), ownerMemberMetaData
.getFullFieldName(), StringUtils.toJVMIDString(elementSM.getObject()), StringUtils.toJVMIDString(elementOwner)));
}
}
}
boolean modified = false;
boolean toBeInserted = true;
if (relationType == Relation.MANY_TO_MANY_BI)
{
// This is an M-N relation so we need to check if the element already has us in its collection
// to avoid duplicate join table entries
toBeInserted = !elementAlreadyContainsOwnerInMtoN(sm, element);
}
if (toBeInserted)
{
try
{
ObjectManager om = sm.getObjectManager();
ManagedConnection mconn = storeMgr.getConnection(om);
try
{
// Add a row to the join table
int orderID = -1;
if (orderMapping != null)
{
orderID = getNextIDForOrderColumn(sm);
}
int[] returnCode = internalAdd(sm, element, mconn, false, orderID, true);
if (returnCode[0] > 0)
{
modified = true;
}
}
finally
{
mconn.release();
}
}
catch (MappedDatastoreException e)
{
NucleusLogger.DATASTORE.error(e);
String msg = LOCALISER.msg("056009", e.getMessage());
NucleusLogger.DATASTORE.error(msg);
throw new NucleusDataStoreException(msg, e);
}
}
return modified;
}
/**
* Adds all elements from a collection to the association container.
* @param sm State Manager for the container.
* @param elements Collection of elements to add
* @return Whether it was successful
*/
public boolean addAll(StateManager sm, Collection elements, int size)
{
if (elements == null || elements.size() == 0)
{
return false;
}
boolean modified = false;
List exceptions = new ArrayList();
boolean batched = (elements.size() > 1);
// Validate all elements for writing
Iterator iter = elements.iterator();
while (iter.hasNext())
{
Object element = iter.next();
validateElementForWriting(sm, element, null);
if (relationType == Relation.ONE_TO_MANY_BI && sm.getObjectManager().getOMFContext()
.getPersistenceConfiguration().getBooleanProperty("datanucleus.manageRelationships"))
{
// Managed Relations : make sure we have consistency of relation
StateManager elementSM = sm.getObjectManager().findStateManager(element);
if (elementSM != null)
{
AbstractMemberMetaData[] relatedMmds = ownerMemberMetaData.getRelatedMemberMetaData(clr);
Object elementOwner = elementSM.provideField(relatedMmds[0].getAbsoluteFieldNumber());
if (elementOwner == null)
{
// No owner, so correct it
NucleusLogger.JDO.info(LOCALISER.msg("056037", StringUtils.toJVMIDString(sm.getObject()), ownerMemberMetaData
.getFullFieldName(), StringUtils.toJVMIDString(elementSM.getObject())));
elementSM.replaceField(relatedMmds[0].getAbsoluteFieldNumber(), sm.getObject(), false);
}
else if (elementOwner != sm.getObject() && sm.getReferencedPC() == null)
{
// Owner of the element is neither this container nor its referenced object
// Inconsistent owner, so throw exception
throw new NucleusUserException(LOCALISER.msg("056038", StringUtils.toJVMIDString(sm.getObject()),
ownerMemberMetaData.getFullFieldName(), StringUtils.toJVMIDString(elementSM.getObject()), StringUtils
.toJVMIDString(elementOwner)));
}
}
}
}
try
{
ObjectManager om = sm.getObjectManager();
ManagedConnection mconn = storeMgr.getConnection(om);
try
{
preGetNextIDForOrderColumn(mconn);
int nextOrderID = 0;
if (orderMapping != null)
{
// Get the order id for the first item
nextOrderID = getNextIDForOrderColumn(sm);
}
// Loop through all elements to be added
iter = elements.iterator();
Object element = null;
while (iter.hasNext())
{
element = iter.next();
try
{
// Add the row to the join table
int[] rc = internalAdd(sm, element, mconn, batched, nextOrderID, !batched || (batched && !iter.hasNext()));
if (rc != null)
{
for (int i = 0; i < rc.length; i++)
{
if (rc[i] > 0)
{
// At least one record was inserted
modified = true;
}
}
}
nextOrderID++;
}
catch (MappedDatastoreException mde)
{
mde.printStackTrace();
exceptions.add(mde);
NucleusLogger.DATASTORE.error(mde);
}
}
}
finally
{
mconn.release();
}
}
catch (MappedDatastoreException e)
{
e.printStackTrace();
exceptions.add(e);
NucleusLogger.DATASTORE.error(e);
}
if (!exceptions.isEmpty())
{
// Throw all exceptions received as the cause of a JPOXDataStoreException so the user can see which
// record(s) didn't persist
String msg = LOCALISER.msg("056009", ((Exception) exceptions.get(0)).getMessage());
NucleusLogger.DATASTORE.error(msg);
throw new NucleusDataStoreException(msg, (Throwable[]) exceptions.toArray(new Throwable[exceptions.size()]), sm.getObject());
}
return modified;
}
protected abstract void preGetNextIDForOrderColumn(ManagedConnection mconn) throws MappedDatastoreException;
/**
* Method to add a row to the join table. Used by add() and addAll() to add a row to the join table.
* @param sm StateManager for the owner of the collection
* @param element The element to add the relation to
* @param conn Connection to use
* @param batched Whether we are batching
* @param orderId The order id to use for this element relation (if ordering is used)
* @param executeNow Whether to execute the statement now (or leave til later)
* @return The return code(s) for any records added. There may be multiple if using batched
* @throws MappedDatastoreException Thrown if an error occurs
*/
private int[] internalAdd(StateManager sm, Object element, ManagedConnection conn, boolean batched, int orderId, boolean executeNow)
throws MappedDatastoreException
{
boolean toBeInserted = true;
if (relationType == Relation.MANY_TO_MANY_BI)
{
// This is an M-N relation so we need to check if the element already has us
// in its collection to avoid duplicate join table entries
// TODO Find a better way of doing this
toBeInserted = !elementAlreadyContainsOwnerInMtoN(sm, element);
}
if (toBeInserted)
{
return doInternalAdd(sm, element, conn, batched, orderId, executeNow);
}
return null;
}
protected abstract int[] doInternalAdd(StateManager sm, Object element, ManagedConnection conn,
boolean batched, int orderId, boolean executeNow) throws MappedDatastoreException;
/**
* Accessor for the next id when elements primary key can't be part of the primary key by datastore limitations like
* BLOB types can't be primary keys. Also for where the user wants to allow duplicates.
* @param sm The State Manager
* @return the next id value
*/
protected abstract int getNextIDForOrderColumn(StateManager sm) throws MappedDatastoreException;
// ---------------------------- Query Methods ------------------------------
/**
* Utility for use in building a query, joining the element table and the owner table.
* @param stmt The Query Statement
* @param parentStmt the parent Query Statement. If no parent, "parentStmt" must be equal to "stmt"
* @param ownerMapping the mapping for the owner
* @param ownerTblExpr Table Expression for the owner
* @param filteredElementType The Class Type for the filtered element
* @param elementExpr The Expression for the element
* @param elementTableAlias The SQL alias to assign to the element table expression
* @param setTableAlias The alias for the "Set" table
* @param existsQuery Whether this is joining for an EXISTS query
* @return Expression for the join
*/
public ScalarExpression joinElementsTo(QueryExpression stmt, QueryExpression parentStmt, JavaTypeMapping ownerMapping,
LogicSetExpression ownerTblExpr, DatastoreIdentifier setTableAlias, Class filteredElementType, ScalarExpression elementExpr,
DatastoreIdentifier elementTableAlias, boolean existsQuery)
{
ClassLoaderResolver clr = stmt.getClassLoaderResolver();
if (!clr.isAssignableFrom(elementType, filteredElementType) && !clr.isAssignableFrom(filteredElementType, elementType))
{
throw new IncompatibleQueryElementTypeException(elementType, filteredElementType.getName());
}
if (!existsQuery)
{
// Not part of an EXISTS subquery
LogicSetExpression ownTblExpr = stmt.newTableExpression(containerTable, setTableAlias);
if (!parentStmt.hasCrossJoin(ownTblExpr) && !stmt.getMainTableExpression().equals(ownTblExpr))
{
// Parent doesnt have the collection table, and not the candidate here so cross join to it
stmt.crossJoin(ownTblExpr, true);
}
// Reverse collection contains query so join back to the owner
ScalarExpression ownerExpr = ownerMapping.newScalarExpression(stmt, ownerTblExpr);
ScalarExpression ownerSetExpr = this.ownerMapping.newScalarExpression(stmt, stmt.getTableExpression(setTableAlias));
stmt.andCondition(ownerExpr.eq(ownerSetExpr), true);
}
if (storeMgr.getMappedTypeManager().isSupportedMappedType(filteredElementType.getName()))
{
// Element = Non-PC(embedded)
return elementMapping.newScalarExpression(stmt, stmt.getTableExpression(setTableAlias));
}
else if (elementsAreEmbedded || elementsAreSerialised)
{
// Element = PC(embedded), PC(serialised)
return elementMapping.newScalarExpression(stmt, stmt.getTableExpression(setTableAlias));
}
else
{
// Element = PC
// Join to element table on id column(s) of element
DatastoreClass elementTable = storeMgr.getDatastoreClass(filteredElementType.getName(), clr);
DatastoreClass joiningClass = (elementExpr.getLogicSetExpression() == null ? elementTable : (DatastoreClass) elementExpr
.getLogicSetExpression().getMainTable());
JavaTypeMapping elementTableID = joiningClass.getIDMapping();
// Get expression for the element table, allowing for it existing in this query or the parent query
LogicSetExpression elmTblExpr = stmt.getTableExpression(elementTableAlias);
if (elmTblExpr == null)
{
// Note : we only use the parentStmt if not an unbound variable. Unbound variables may be registered
// with the parent but not yet bound so ignore the parent for those
if (!(elementExpr instanceof UnboundVariable) && parentStmt != stmt)
{
elmTblExpr = parentStmt.getTableExpression(elementTableAlias);
}
if (elmTblExpr == null)
{
// Table not present in subquery or parent query so add table
elmTblExpr = stmt.newTableExpression(elementTable, elementTableAlias);
}
}
if (!parentStmt.getMainTableExpression().equals(elmTblExpr) && !parentStmt.hasCrossJoin(elmTblExpr))
{
// Element table not present in parent query so add cross join to it in the subquery
stmt.crossJoin(elmTblExpr, true);
}
ScalarExpression elmSetExpr = elementMapping.newScalarExpression(stmt, stmt.getTableExpression(setTableAlias));
if (elementExpr.getLogicSetExpression() != null && !elementTable.equals(elementExpr.getLogicSetExpression().getMainTable()))
{
// elementExpr might express a FK in another to the ELEMENT table
if (existsQuery)
{
// Exists subquery so apply "and" condition direct rather than returning it
stmt.andCondition(elmSetExpr.eq(elementExpr), true);
return elmSetExpr;
}
else
{
// Return the expression to join the element to
return elmSetExpr;
}
}
else
{
// elementExpr might be a PK of the ELEMENT table
if (existsQuery)
{
// Exists subquery so apply "and" condition direct rather than returning it
ScalarExpression elementIdExpr = elementTableID.newScalarExpression(stmt, elmTblExpr);
stmt.andCondition(elmSetExpr.eq(elementIdExpr), true);
return elementIdExpr;
}
else
{
// Return the expression to join the element to
return elmSetExpr;
}
}
}
}
}