com.techempower.cache.CachedRelation Maven / Gradle / Ivy
Show all versions of gemini Show documentation
/*******************************************************************************
* Copyright (c) 2018, TechEmpower, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name TechEmpower, Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package com.techempower.cache;
import gnu.trove.set.*;
import gnu.trove.set.hash.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.locks.*;
import com.techempower.collection.relation.*;
import com.techempower.data.*;
import com.techempower.gemini.cluster.*;
import com.techempower.helper.*;
import com.techempower.util.*;
/**
* A thread-safe {@link EntityRelation} that caches its contents and enforces
* the relation type (many-to-many, etc.) with an {@link LongRelation}.
*
* Since more than one relation can exist for a given pair of classes, it's
* likely that you'll want to retain a reference to the relation in your cache.
* Here are examples of registering relations:
*
*
* private EntityRelation<Foo, Bar> mapFooToBar;
* private EntityRelation<Bar, Baz> mapBarToBaz;
*
* public void initialize()
* {
* // A many-to-many relation stored in the table "mapfootobar", with columns
* // "foo" and "bar". If you follow that naming convention for your relation
* // tables, then your calls to register will look like this.
* mapFooToBar = register(CachedRelation.of(Foo.class, Bar.class));
*
* // A one-to-one relation stored in the table "BarsAndBazzes", with columns
* // "BarID" and "BazID". Your calls to register will look like this if you
* // want a relation type other than many-to-many or you follow non-standard
* // naming conventions for your relation tables.
* mapBarToBaz = register(CachedRelation.of(Bar.class, Baz.class)
* .table("BarsAndBazzes")
* .leftColumn("BarID")
* .rightColumn("BazID")
* .relation(new OneToOneLongRelation(true)));
* }
*
*
* @param the type of the left values in this relation
* @param the type of the right values in this relation
*
* @see com.techempower.collection.relation.LongRelation
*/
public class CachedRelation
implements EntityRelation, Identifiable
{
// TODO: Consider whether database-level uniqueness constraints and "INSERT
// IGNORE" behavior should be supported. This shouldn't matter for
// single-server applications, but clustered applications could get into
// situations where simultaneous writes cause bad data to be inserted.
//
// Constants
//
/**
* This is the largest number of pairs to be inserted or deleted in a single
* SQL statement. MySQL breaks on extremely large SQL statements, so this
* provides a safe upper limit.
*/
private static final int MAX_SQL_SIZE = 1000;
//
// Static factories
//
/**
* Creates a new {@link Builder}, which is used to construct a
* {@link CachedRelation}. Example usage:
*
*
* CachedRelation<Foo, Bar> = CachedRelation.of(Foo.class, Bar.class) // new Builder
* .table("MapFooToBar") // modified Builder
* .leftColumn("FooID") // modified Builder
* .rightColumn("BarID") // modified Builder
* .build(controller); // new CachedRelation
*
*
* Note that a {@link EntityStore#register(EntityGroup.Builder)} method exists, and
* it returns a {@link CachedRelation}, so in most cases calling
* {@code .build(controller)} is unnecessary. For example:
*
*
* mapFooToBar = register(CachedRelation.of(Foo.class, Bar.class) // new Builder
* .table("MapFooToBar") // modified Builder
* .leftColumn("FooID") // modified Builder
* .rightColumn("BarID") // modified Builder
* ); // the register method calls .build(controller) for us and returns the result
*
*
* @param leftType the type of the left values in this relation
* @param rightType the type of the right values in this relation
* @return A new {@link Builder}.
*/
public static Builder of(
Class leftType, Class rightType)
{
return new Builder<>(leftType, rightType);
}
//
// Fields
//
private final EntityStore store;
private final ConnectorFactory cf;
private final Class leftType;
private final Class rightType;
private final String table;
private final String leftColumn;
private final String rightColumn;
private final String quotedTable;
private final String quotedLeftColumn;
private final String quotedRightColumn;
private final LongRelation relation;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Collection listeners
= new ArrayList<>();
private volatile boolean loaded = false;
/**
* A unique identifier for this cached relation to be assigned by the entity
* store as the relation is registered.
*/
private long id;
//
// Constructors
//
/**
* Constructs a new relation with the specified parameters. This constructor
* is non-public because users should only instantiate this class by way of a
* {@link Builder}, which can be obtained from a call to
* {@link CachedRelation#of(Class, Class)}.
*
* @param leftType the type of the left values in this relation
* @param rightType the type of the right values in this relation
* @param store the store that manages the objects in this relation
* @param tableName the name of the table in the database that stores this
* relation
* @param leftColumn the name of the column that stores the identities of
* the left values
* @param rightColumn the name of the column that stores the identities of
* the right values
* @param relation the type of relation between left and right values, e.g.
* many-to-many, one-to-many...
*/
protected CachedRelation(EntityStore store,
Class leftType, Class rightType,
String tableName, String leftColumn, String rightColumn,
LongRelation relation)
{
this.store = store;
this.cf = store.getConnectorFactory();
this.leftType = leftType;
this.rightType = rightType;
this.table = (tableName == null)
? leftType.getSimpleName().toLowerCase()
+ "to" + rightType.getSimpleName().toLowerCase()
: tableName;
this.leftColumn = (leftColumn == null)
? leftType.getSimpleName().toLowerCase() + "id"
: leftColumn;
this.rightColumn = (rightColumn == null)
? rightType.getSimpleName().toLowerCase() + "id"
: rightColumn;
this.relation = (relation == null)
? new ManyToManyLongRelation(true)
: (LongRelation)relation.clone(); // Defensive copy.
this.quotedTable = DatabaseHelper.quoteTableOrColumn(
this.cf, this.table);
this.quotedLeftColumn = DatabaseHelper.quoteTableOrColumn(
this.cf, this.leftColumn);
this.quotedRightColumn = DatabaseHelper.quoteTableOrColumn(
this.cf, this.rightColumn);
}
//
// Public API
//
@Override
public boolean add(long leftID, long rightID)
{
return add(leftID, rightID, true, true, true);
}
/**
* Adds the specified pair to the relation.
*
* @param leftID the ID of the left value of the pair to be added
* @param rightID the ID of the right value of the pair to be added
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation changed as a result of the call
*/
public boolean add(long leftID, long rightID, boolean updateDatabase, boolean notifyListeners, boolean notifyDistributionListeners)
{
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
if (!this.relation.add(leftID, rightID))
{
return false;
}
if (updateDatabase)
{
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement insertStatement = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + ", " + quotedRightColumn
+ ") VALUES (?, ?);"))
{
insertStatement.setLong(1, leftID);
insertStatement.setLong(2, rightID);
insertStatement.executeUpdate();
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.add(this.id, leftID, rightID);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while adding relation, left " + leftID
+ " and right " + rightID, e);
return false;
}
finally
{
this.lock.writeLock().unlock();
}
}
@Override
public boolean add(long leftID, R right)
{
return (right != null
&& add(leftID, right.getId()));
}
@Override
public boolean add(L left, long rightID)
{
return (left != null
&& add(left.getId(), rightID));
}
@Override
public boolean add(L left, R right)
{
return (left != null
&& right != null
&& add(left.getId(), right.getId()));
}
@Override
public boolean addAll(LongRelation relationToAdd)
{
return addAll(relationToAdd, true, true, true);
}
/**
* Adds the given pairs to the relation and updates the database.
*
* @param relationToAdd the pairs to be added
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation changed as a result of the call
*/
public boolean addAll(LongRelation relationToAdd, boolean updateDatabase,
boolean notifyListeners, boolean notifyDistributionListeners)
{
if (relationToAdd == null)
{
return false;
}
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
long[] newLefts = new long[relationToAdd.size()];
long[] newRights = new long[relationToAdd.size()];
int insertCount = 0;
for (LongRelationIterator iter = relationToAdd.iterator(); iter.hasNext();)
{
iter.next();
long leftID = iter.left();
long rightID = iter.right();
if (this.relation.add(leftID, rightID))
{
newLefts[insertCount] = leftID;
newRights[insertCount] = rightID;
insertCount++;
}
}
if (insertCount == 0)
{
// No changes. We can leave now.
return false;
}
if (updateDatabase)
{
// Release the write lock because all that's left is updating the DB.
this.lock.writeLock().unlock();
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
if (insertCount <= MAX_SQL_SIZE)
{
// Easy. We just run one statement.
try (PreparedStatement insertStatementA = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") VALUES "
+ StringHelper.join(",", Collections.nCopies(insertCount, "(?,?)"))
+ ";"))
{
for (int i = 0; i < insertCount; i++)
{
insertStatementA.setLong(2 * i + 1, newLefts[i]);
insertStatementA.setLong(2 * i + 2, newRights[i]);
}
insertStatementA.executeUpdate();
}
}
else
{
// Darn, we'll have to run a bunch of statements. First, a bunch of
// large ones in a batch, then a smaller one for the remaining pairs.
int smallInsertSize = insertCount % MAX_SQL_SIZE;
int lastBigInsertIndex = (insertCount - smallInsertSize);
int numLargeInserts = lastBigInsertIndex / MAX_SQL_SIZE;
try (PreparedStatement insertStatementA = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") VALUES "
+ StringHelper.join(",", Collections.nCopies(MAX_SQL_SIZE, "(?,?)"))
+ ";"))
{
for (int i = 0; i < numLargeInserts; i++)
{
int startIndex = i * MAX_SQL_SIZE;
for (int j = 0; j < MAX_SQL_SIZE; j++)
{
insertStatementA.setLong(2 * j + 1, newLefts[j + startIndex]);
insertStatementA.setLong(2 * j + 2, newRights[j + startIndex]);
}
insertStatementA.addBatch();
}
insertStatementA.executeBatch();
}
// Now pick up any stragglers.
if (smallInsertSize > 0)
{
try (PreparedStatement insertStatementB = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") VALUES "
+ StringHelper.join(",", Collections.nCopies(smallInsertSize, "(?,?)"))
+ ";"))
{
for (int i = 0; i < smallInsertSize; i++)
{
insertStatementB.setLong(2 * i + 1, newLefts[i + lastBigInsertIndex]);
insertStatementB.setLong(2 * i + 2, newRights[i + lastBigInsertIndex]);
}
insertStatementB.executeUpdate();
}
}
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.addAll(this.id, relationToAdd);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while adding relations.", e);
return false;
}
finally
{
try
{
this.lock.writeLock().unlock();
}
catch (IllegalMonitorStateException exc)
{
// That's ok, it just means we already unlocked it prior to updating
// the database.
}
}
}
/**
* Adds the given listener to the relation.
*
* @param listener the listener to be added
*/
public void addListener(CachedRelationListener listener)
{
this.listeners.add(listener);
}
@Override
public void clear()
{
clear(true, true, true);
}
/**
* Clears the relation of all pairs.
*
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
*/
public void clear(boolean updateDatabase, boolean notifyListeners, boolean notifyDistributionListeners)
{
this.lock.writeLock().lock();
try
{
this.relation.clear();
this.lock.writeLock().unlock();
if (updateDatabase)
{
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement deleteStatement = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + ";"))
{
deleteStatement.executeUpdate();
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.clear(this.id);
}
}
}
this.loaded = true;
}
catch (SQLException e)
{
store.getLog().log("Exception while clearing relations.", e);
}
finally
{
try
{
this.lock.writeLock().unlock();
}
catch (IllegalMonitorStateException exc)
{
// That's ok, it just means we already unlocked it in deferDatabaseUpdates.
}
}
}
@Override
public boolean contains(long leftID, long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.contains(leftID, rightID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public boolean contains(long leftID, R right)
{
return (right != null
&& contains(leftID, right.getId()));
}
@Override
public boolean contains(L left, long rightID)
{
return (left != null
&& contains(left.getId(), rightID));
}
@Override
public boolean contains(L left, R right)
{
return (left != null
&& right != null
&& contains(left.getId(), right.getId()));
}
@Override
public boolean containsLeftValue(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.containsLeftValue(leftID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public boolean containsLeftValue(L left)
{
return (left != null
&& containsLeftValue(left.getId()));
}
@Override
public boolean containsRightValue(long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.containsRightValue(rightID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public boolean containsRightValue(R right)
{
return (right != null
&& containsRightValue(right.getId()));
}
@Override
public long[] leftIDArray(R right)
{
return (right == null)
? new long[0]
: leftIDArray(right.getId());
}
@Override
public long[] leftIDArray(long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.leftValues(rightID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Set leftIDs(long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
Set leftIDs = new HashSet<>();
for (long i : this.relation.leftValues(rightID))
{
leftIDs.add(i);
}
return leftIDs;
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Set leftIDs(R right)
{
return (right == null)
? Collections.emptySet()
: leftIDs(right.getId());
}
@Override
public int leftSize(long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.leftSize(rightID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public int leftSize(R right)
{
return (right == null)
? 0
: leftSize(right.getId());
}
@Override
public Class leftType()
{
return this.leftType;
}
@Override
public List leftValueList(long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.store.list(this.leftType,
this.relation.leftValues(rightID));
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public List leftValueList(R right)
{
return (right == null)
? Collections.emptyList()
: leftValueList(right.getId());
}
@Override
public Set leftValueSet(long rightID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return new HashSet<>(this.store.list(this.leftType,
this.relation.leftValues(rightID)));
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Set leftValueSet(R right)
{
return (right == null)
? Collections.emptySet()
: leftValueSet(right.getId());
}
/**
* Returns a copy of the list of listeners to this relation.
*
* @return A copy of the list of listeners to this relation.
*/
public List listeners()
{
return new ArrayList<>(this.listeners);
}
/**
* Loads the contents of this relation from the database.
*/
protected void load()
{
this.lock.writeLock().lock();
try
{
if (this.loaded)
{
return;
}
this.relation.clear();
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement selectStatement = monitor.getConnection().prepareStatement(
"SELECT " + quotedLeftColumn + ", "
+ quotedRightColumn + " FROM " + quotedTable
+ ";"))
{
try (ResultSet resultSet = selectStatement.executeQuery())
{
while (resultSet.next())
{
long leftID = resultSet.getLong(this.leftColumn);
long rightID = resultSet.getLong(this.rightColumn);
this.relation.add(leftID, rightID);
}
}
}
}
this.loaded = true;
}
catch (SQLException e)
{
store.getLog().log("Exception while loading relations.", e);
}
finally
{
this.lock.writeLock().unlock();
}
}
@Override
public LongRelation relation()
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return (LongRelation)this.relation.clone();
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public boolean remove(long leftID, long rightID)
{
return remove(leftID, rightID, true, true, true);
}
@Override
public boolean removeEntity(T object)
{
return (object == null)
? false
: removeEntity(object.getClass(), object.getId());
}
@Override
public boolean removeEntity(Class type, long idToRemove)
{
boolean changed = false;
if (this.leftType == type)
{
changed = (removeLeftValue(idToRemove) || changed);
}
if (this.rightType == type)
{
changed = (removeRightValue(idToRemove) || changed);
}
return changed;
}
/**
* Removes the specified pair of values from this relation.
*
* @param leftID the id of the left value of the pair to be removed
* @param rightID the id of the right value of the pair to be removed
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation was modified as a result of the call
*/
public boolean remove(long leftID, long rightID, boolean updateDatabase, boolean notifyListeners, boolean notifyDistributionListeners)
{
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
if (!this.relation.remove(leftID, rightID))
{
return false;
}
if (updateDatabase)
{
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement deleteStatement = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + " WHERE "
+ quotedLeftColumn + " = ? AND "
+ quotedRightColumn + " = ?;"))
{
deleteStatement.setLong(1, leftID);
deleteStatement.setLong(2, rightID);
deleteStatement.executeUpdate();
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.remove(this.id, leftID, rightID);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while removing relation, left " + leftID
+ " and right " + rightID + ".", e);
return false;
}
finally
{
this.lock.writeLock().unlock();
}
}
@Override
public boolean remove(long leftID, R right)
{
return (right != null
&& remove(leftID, right.getId()));
}
@Override
public boolean remove(L left, long rightID)
{
return (left != null
&& remove(left.getId(), rightID));
}
@Override
public boolean remove(L left, R right)
{
return (left != null
&& right != null
&& remove(left.getId(), right.getId()));
}
@Override
public boolean removeAll(LongRelation relationToRemove)
{
return removeAll(relationToRemove, true, true, true);
}
/**
* Removes the given pairs from the relation and updates the database.
*
* @param relationToRemove the pairs to be removed
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation changed as a result of the call
*/
public boolean removeAll(LongRelation relationToRemove, boolean updateDatabase,
boolean notifyListeners, boolean notifyDistributionListeners)
{
if (relationToRemove == null)
{
return false;
}
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
long[] removedLefts = new long[relationToRemove.size()];
long[] removedRights = new long[relationToRemove.size()];
int deleteCount = 0;
for (LongRelationIterator iter = relationToRemove.iterator(); iter.hasNext();)
{
iter.next();
long leftID = iter.left();
long rightID = iter.right();
if (this.relation.remove(leftID, rightID))
{
removedLefts[deleteCount] = leftID;
removedRights[deleteCount] = rightID;
deleteCount++;
}
}
if (deleteCount == 0)
{
// No changes. We can leave now.
return false;
}
if (updateDatabase)
{
// Release the write lock because all that's left is updating the DB.
this.lock.writeLock().unlock();
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
if (deleteCount <= MAX_SQL_SIZE)
{
// Easy. We just run one statement.
try (PreparedStatement deleteStatementA = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + " WHERE ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") IN ("
+ StringHelper.join(",", Collections.nCopies(deleteCount, "(?,?)"))
+ ");"))
{
for (int i = 0; i < deleteCount; i++)
{
deleteStatementA.setLong(2 * i + 1, removedLefts[i]);
deleteStatementA.setLong(2 * i + 2, removedRights[i]);
}
deleteStatementA.executeUpdate();
}
}
else
{
// Darn, we'll have to run a bunch of statements. First, a bunch of
// large ones in a batch, then a smaller one for the remaining pairs.
int smallDeleteSize = deleteCount % MAX_SQL_SIZE;
int lastBigDeleteIndex = (deleteCount - smallDeleteSize);
int numLargeDeletes = lastBigDeleteIndex / MAX_SQL_SIZE;
try (PreparedStatement deleteStatementA = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + " WHERE ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") IN ("
+ StringHelper.join(",", Collections.nCopies(MAX_SQL_SIZE, "(?,?)"))
+ ");"))
{
for (int i = 0; i < numLargeDeletes; i++)
{
int startIndex = i * MAX_SQL_SIZE;
for (int j = 0; j < MAX_SQL_SIZE; j++)
{
deleteStatementA.setLong(2 * j + 1, removedLefts[j + startIndex]);
deleteStatementA.setLong(2 * j + 2, removedRights[j + startIndex]);
}
deleteStatementA.addBatch();
}
deleteStatementA.executeBatch();
}
// Now pick up any stragglers.
if (smallDeleteSize > 0)
{
try (PreparedStatement deleteStatementB = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + " WHERE ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") IN ("
+ StringHelper.join(",", Collections.nCopies(smallDeleteSize, "(?,?)"))
+ ");"))
{
for (int i = 0; i < smallDeleteSize; i++)
{
deleteStatementB.setLong(2 * i + 1, removedLefts[i + lastBigDeleteIndex]);
deleteStatementB.setLong(2 * i + 2, removedRights[i + lastBigDeleteIndex]);
}
deleteStatementB.executeUpdate();
}
}
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.addAll(this.id, relationToRemove);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while removing relations.", e);
return false;
}
finally
{
try
{
this.lock.writeLock().unlock();
}
catch (IllegalMonitorStateException exc)
{
// That's ok, it just means we already unlocked it prior to updating
// the database.
}
}
}
@Override
public boolean removeLeftValue(long leftID)
{
return removeLeftValue(leftID, true, true, true);
}
/**
* Removes the specified left value from this relation.
*
* @param leftID the id of the left value to be removed from this relation
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation was modified as a result of the call
*/
public boolean removeLeftValue(long leftID, boolean updateDatabase, boolean notifyListeners, boolean notifyDistributionListeners)
{
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
if (!this.relation.removeLeftValue(leftID))
{
return false;
}
if (updateDatabase)
{
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement deleteStatement = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + " WHERE "
+ quotedLeftColumn + " = ?;"))
{
deleteStatement.setLong(1, leftID);
deleteStatement.executeUpdate();
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.removeLeftValue(this.id, leftID);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while removing all for left " + leftID + ".", e);
return false;
}
finally
{
this.lock.writeLock().unlock();
}
}
@Override
public boolean removeLeftValue(L left)
{
return (left != null
&& removeLeftValue(left.getId()));
}
@Override
public boolean removeRightValue(long rightID)
{
return removeRightValue(rightID, true, true, true);
}
/**
* Removes the specified right value from this relation.
*
* @param rightID the id of the right value to be removed from this
* relation
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation was modified as a result of the call
*/
public boolean removeRightValue(long rightID, boolean updateDatabase, boolean notifyListeners, boolean notifyDistributionListeners)
{
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
if (!this.relation.removeRightValue(rightID))
{
return false;
}
if (updateDatabase)
{
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement deleteStatement = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + " WHERE "
+ quotedRightColumn + " = ?;"))
{
deleteStatement.setLong(1, rightID);
deleteStatement.executeUpdate();
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.removeRightValue(this.id, rightID);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while removing all for right " + rightID + ".", e);
return false;
}
finally
{
this.lock.writeLock().unlock();
}
}
@Override
public boolean removeRightValue(R right)
{
return (right != null
&& removeRightValue(right.getId()));
}
/**
* {@inheritDoc}
*
* If called with a relation that is equivalent to the current relation,
* this function will immediately return with a value of false and will
* not hit the db or notify listeners.
*/
@Override
public boolean replaceAll(LongRelation relationToReplace)
{
return replaceAll(relationToReplace, true, true, true);
}
/**
* Clears the existing relation, then sets the relation to the passed in
* relation.
*
* Note that this is generally preferable to doing the following:
*
*
* {@code
* // foo is a CachedRelation.
* foo.clear();
* foo.addAll( .. );
* }
*
*
* The above will cause foo to be empty for a certain window of time.
* Using replaceAll( .. ) will achieve the same end goal of clearing the
* current relation and then adding the passed in relation, but will never
* result in a call to this object seeing an empty relation.
*
* This call will only block reads very briefly while switching to the
* new cached relation and notifying listeners. If deferDatabaseUpdates is
* false, then this blocking extends until the database writes have
* completed.
*
* If called with a relation that is equivalent to the current relation,
* this function will immediately return with a value of false and will
* not hit the db or notify listeners.
*
* @param relationToReplace the pairs to be added
* @param updateDatabase whether to update the database
* @param notifyListeners whether to notify the listeners of the change
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
* @return true if the relation changed as a result of the call
*/
public boolean replaceAll(LongRelation relationToReplace, boolean updateDatabase,
boolean notifyListeners, boolean notifyDistributionListeners)
{
if (relationToReplace == null)
{
return false;
}
if (!this.loaded)
{
load();
}
this.lock.writeLock().lock();
try
{
if (this.relation.containsAll(relationToReplace)
&& relationToReplace.containsAll(this.relation))
{
// The relations are equivalent so this is a no-op.
return false;
}
this.relation.clear();
long[] newLefts = new long[relationToReplace.size()];
long[] newRights = new long[relationToReplace.size()];
int insertCount = 0;
for (LongRelationIterator iter = relationToReplace.iterator(); iter.hasNext();)
{
iter.next();
long leftID = iter.left();
long rightID = iter.right();
if (this.relation.add(leftID, rightID))
{
newLefts[insertCount] = leftID;
newRights[insertCount] = rightID;
insertCount++;
}
}
if (updateDatabase)
{
// Release the write lock because all that's left is updating the DB.
this.lock.writeLock().unlock();
try (ConnectionMonitor monitor = this.cf.getConnectionMonitor())
{
try (PreparedStatement deleteStatement = monitor.getConnection().prepareStatement(
"DELETE FROM " + quotedTable + ";"))
{
deleteStatement.executeUpdate();
}
if (insertCount > 0)
{
if (insertCount <= MAX_SQL_SIZE)
{
// Easy. We just run one statement.
try (PreparedStatement insertStatementA = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") VALUES "
+ StringHelper.join(",", Collections.nCopies(insertCount, "(?,?)"))
+ ";"))
{
for (int i = 0; i < insertCount; i++)
{
insertStatementA.setLong(2 * i + 1, newLefts[i]);
insertStatementA.setLong(2 * i + 2, newRights[i]);
}
insertStatementA.executeUpdate();
}
}
else
{
// Darn, we'll have to run a bunch of statements. First, a bunch of
// large ones in a batch, then a smaller one for the remaining pairs.
int smallInsertSize = insertCount % MAX_SQL_SIZE;
int lastBigInsertIndex = (insertCount - smallInsertSize);
int numLargeInserts = lastBigInsertIndex / MAX_SQL_SIZE;
int count = 0;
try (PreparedStatement insertStatementA = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") VALUES "
+ StringHelper.join(",", Collections.nCopies(MAX_SQL_SIZE, "(?,?)"))
+ ";"))
{
for (int i = 0; i < numLargeInserts; i++)
{
int startIndex = i * MAX_SQL_SIZE;
for (int j = 0; j < MAX_SQL_SIZE; j++)
{
insertStatementA.setLong(2 * j + 1, newLefts[j + startIndex]);
insertStatementA.setLong(2 * j + 2, newRights[j + startIndex]);
}
insertStatementA.addBatch();
// So many statements that we should batch-this-batch.
if (++count % 100 == 0)
{
insertStatementA.executeBatch();
}
}
insertStatementA.executeBatch();
}
// Now pick up any stragglers.
if (smallInsertSize > 0)
{
try (PreparedStatement insertStatementB = monitor.getConnection().prepareStatement(
"INSERT INTO " + quotedTable + " ("
+ quotedLeftColumn + "," + quotedRightColumn
+ ") VALUES "
+ StringHelper.join(",", Collections.nCopies(smallInsertSize, "(?,?)"))
+ ";"))
{
for (int i = 0; i < smallInsertSize; i++)
{
insertStatementB.setLong(2 * i + 1, newLefts[i + lastBigInsertIndex]);
insertStatementB.setLong(2 * i + 2, newRights[i + lastBigInsertIndex]);
}
insertStatementB.executeUpdate();
}
}
}
}
}
}
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.addAll(this.id, relationToReplace);
}
}
}
return true;
}
catch (SQLException e)
{
store.getLog().log("Exception while replacing relations.", e);
return false;
}
finally
{
try
{
this.lock.writeLock().unlock();
}
catch (IllegalMonitorStateException exc)
{
// That's ok, it just means we already unlocked it prior to updating
// the database.
}
}
}
/**
* Internally marks this relation as "not loaded", which is understood to
* mean that it should be reloaded from the database before being used again.
*/
public void reset()
{
reset(true, true);
}
/**
* Internally marks this relation as "not loaded", which is understood to
* mean that it should be reloaded from the database before being used
* again.
*
* @param notifyListeners whether to notify the listeners of the reset
* @param notifyDistributionListeners Whether to notify any
* DistributionListeners; only used When notifyListeners is true.
*/
public void reset(boolean notifyListeners, boolean notifyDistributionListeners)
{
this.lock.writeLock().lock();
try
{
this.loaded = false;
if (notifyListeners)
{
for (CachedRelationListener listener : this.listeners)
{
if (!(listener instanceof DistributionListener)
|| notifyDistributionListeners)
{
listener.reset(this.id);
}
}
}
}
finally
{
this.lock.writeLock().unlock();
}
}
/**
* Resets this relation if it maps objects of the specified type.
*
* @param type The type of the cache group being reset.
*/
public void reset(Class type)
{
reset(type, true, true);
}
/**
* Resets this relation if it maps objects of the specified type.
*
* @param type The type of the cache group being reset.
* @param notifyListeners whether to notify the listeners of the reset
*/
public void reset(Class type, boolean notifyListeners, boolean notifyDistributionListeners)
{
if (type.equals(this.leftType)
|| type.equals(this.rightType))
{
reset(notifyListeners, notifyDistributionListeners);
}
}
@Override
public long[] rightIDArray(L left)
{
return (left == null)
? new long[0]
: rightIDArray(left.getId());
}
@Override
public long[] rightIDArray(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.rightValues(leftID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public TLongSet rightIDsLongSet(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.rightValuesLongSet(leftID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Set rightIDs(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
Set rightIDs = new HashSet<>();
for (long i : this.relation.rightValues(leftID))
{
rightIDs.add(i);
}
return rightIDs;
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Set rightIDs(L left)
{
return (left == null)
? Collections.emptySet()
: rightIDs(left.getId());
}
@Override
public TLongSet rightIDsLongSet(L left)
{
return (left == null)
? new TLongHashSet(0)
: rightIDsLongSet(left.getId());
}
@Override
public int rightSize(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.rightSize(leftID);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public int rightSize(L left)
{
return (left == null)
? 0
: rightSize(left.getId(), (Collection)null);
}
@Override
public int rightSize(L left, Collection filterRightIds)
{
return (left == null)
? 0
: rightSize(left.getId(), filterRightIds);
}
@Override
public int rightSize(L left, TLongSet filterRightIds)
{
return (left == null)
? 0
: rightSize(left.getId(), filterRightIds);
}
@Override
public int rightSize(long leftID, Collection filterRightIds)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.rightSize(leftID, filterRightIds);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public int rightSize(long leftID, TLongSet filterRightIds)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.rightSize(leftID, filterRightIds);
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Class rightType()
{
return this.rightType;
}
@Override
public List rightValueList(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.store.list(this.rightType,
this.relation.rightValues(leftID));
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public List rightValueList(L left)
{
return (left == null)
? Collections.emptyList()
: rightValueList(left.getId());
}
@Override
public Set rightValueSet(long leftID)
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return new HashSet<>(this.store.list(this.rightType,
this.relation.rightValues(leftID)));
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public Set rightValueSet(L left)
{
return (left == null)
? Collections.emptySet()
: rightValueSet(left.getId());
}
@Override
public int size()
{
if (!this.loaded)
{
load();
}
this.lock.readLock().lock();
try
{
return this.relation.size();
}
finally
{
this.lock.readLock().unlock();
}
}
@Override
public long getId()
{
return this.id;
}
@Override
public void setId(long identity)
{
this.id = identity;
}
@Override
public String tableName()
{
return this.table;
}
@Override
public String toString()
{
return "CachedRelation ["
+ leftType.getSimpleName()
+ "," + rightType.getSimpleName()
+ "]";
}
//
// Inner classes
//
/**
* Creates new instances of {@link CachedRelation}.
*
* @param the type of the left values in the relation
* @param the type of the right values in the relation
*/
public static class Builder
implements EntityRelation.Builder>
{
private final Class leftType;
private final Class rightType;
private String table;
private String leftColumn;
private String rightColumn;
private LongRelation relation;
/**
* Returns a new builder of {@link CachedRelation} instances.
*
* @param leftType The type of left objects.
* @param rightType The type of right objects.
*/
protected Builder(Class leftType, Class rightType)
{
Objects.requireNonNull(leftType);
Objects.requireNonNull(rightType);
this.leftType = leftType;
this.rightType = rightType;
}
@Override
public CachedRelation build(EntityStore store)
{
Objects.requireNonNull(store);
return new CachedRelation<>(
store,
this.leftType,
this.rightType,
this.table,
this.leftColumn,
this.rightColumn,
this.relation);
}
/**
* Sets the name of the table in the database that stores the relation.
*/
public Builder table(String tableName)
{
Objects.requireNonNull(tableName);
this.table = tableName;
return this;
}
/**
* Sets the name of the column in the database that holds the left ids in
* the relation.
*/
public Builder leftColumn(String leftColumnName)
{
Objects.requireNonNull(leftColumnName);
this.leftColumn = leftColumnName;
return this;
}
/**
* Sets the name of the column in the database that holds the right ids in
* the relation.
*/
public Builder rightColumn(String rightColumnName)
{
Objects.requireNonNull(rightColumnName);
this.rightColumn = rightColumnName;
return this;
}
/**
* Sets the relation type.
*
* @see ManyToManyLongRelation
* @see ManyToOneLongRelation
* @see OneToManyLongRelation
* @see OneToOneLongRelation
*/
public Builder relation(LongRelation longRelation)
{
Objects.requireNonNull(longRelation);
this.relation = longRelation;
return this;
}
}
}