Skip to content

Commit e1dd7cf

Browse files
committed
proper deletion handling
1 parent c1eadb7 commit e1dd7cf

30 files changed

Lines changed: 1269 additions & 167 deletions

src/main/java/net/staticstudios/data/DataManager.java

Lines changed: 151 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.staticstudios.data;
22

33
import com.google.common.base.Preconditions;
4+
import com.google.common.base.Predicate;
45
import com.google.common.collect.ArrayListMultimap;
56
import com.google.common.collect.HashMultimap;
67
import com.google.common.collect.Multimap;
@@ -13,7 +14,6 @@
1314
import net.staticstudios.data.data.UniqueData;
1415
import net.staticstudios.data.data.collection.PersistentManyToManyCollection;
1516
import net.staticstudios.data.data.collection.PersistentUniqueDataCollection;
16-
import net.staticstudios.data.data.collection.PersistentValueCollection;
1717
import net.staticstudios.data.data.collection.SimplePersistentCollection;
1818
import net.staticstudios.data.data.value.Value;
1919
import net.staticstudios.data.data.value.persistent.InitialPersistentValue;
@@ -51,6 +51,7 @@
5151
import java.util.*;
5252
import java.util.concurrent.ConcurrentHashMap;
5353
import java.util.concurrent.CopyOnWriteArrayList;
54+
import java.util.stream.Collectors;
5455

5556
public class DataManager extends SQLLogger {
5657
private static final Object NULL_MARKER = new Object();
@@ -165,10 +166,22 @@ public Collection<UniqueData> getDummyUniqueData(String schemaTable) {
165166
return dummyUniqueDataMap.get(schemaTable);
166167
}
167168

169+
public UniqueData getDummyInstance(Class<?> clazz) {
170+
return dummyInstances.get(clazz);
171+
}
172+
168173
public Collection<SimplePersistentCollection<?>> getDummyPersistentCollections(String schemaTable) {
169174
return dummySimplePersistentCollectionMap.get(schemaTable);
170175
}
171176

177+
public Collection<PersistentManyToManyCollection<?>> getDummyPersistentManyToManyCollection(String schemaTable) {
178+
return dummyPersistentManyToManyCollectionMap.get(schemaTable);
179+
}
180+
181+
public Collection<PersistentManyToManyCollection<?>> getAllDummyPersistentManyToManyCollections() {
182+
return new HashSet<>(dummyPersistentManyToManyCollectionMap.values());
183+
}
184+
172185
@Blocking
173186
public <T extends UniqueData> List<T> loadAll(Class<T> clazz) {
174187
logger.debug("Registering: {}", clazz.getName());
@@ -388,6 +401,7 @@ public <T extends UniqueData> List<T> loadAll(Class<T> clazz) {
388401

389402
private void insertIntoCache(InsertContext context) {
390403
addUniqueData(context.holder());
404+
//todo: REFACTOR: similar to deletions, delegate this to the managers
391405

392406
for (InitialPersistentValue data : context.initialPersistentValues().values()) {
393407
UniqueData pvHolder = data.getValue().getHolder().getRootHolder();
@@ -433,81 +447,157 @@ private void insertIntoCache(InsertContext context) {
433447
}
434448

435449
private void insertIntoDataSource(Connection connection, Jedis jedis, InsertContext context) throws SQLException {
436-
persistentValueManager.insertInDatabase(connection, context.holder(), new ArrayList<>(context.initialPersistentValues().values()));
437-
cachedValueManager.setInRedis(jedis, new ArrayList<>(context.initialCachedValues().values()));
438-
}
439-
440-
public <T extends UniqueData> void delete(T holder) {
441-
List<Data<?>> dataList = new ArrayList<>();
450+
if (context.initialPersistentValues().isEmpty() && context.initialCachedValues().isEmpty()) {
451+
String sql = "INSERT INTO " + context.holder().getSchema() + "." + context.holder().getTable() + " (" + context.holder().getIdentifier().getColumn() + ") VALUES (?)";
452+
logSQL(sql);
442453

443-
for (Field field : ReflectionUtils.getFields(holder.getClass())) {
444-
field.setAccessible(true);
445-
446-
if (Data.class.isAssignableFrom(field.getType())) {
447-
try {
448-
Data<?> data = (Data<?>) field.get(holder);
449-
dataList.add(data);
450-
} catch (IllegalAccessException e) {
451-
throw new RuntimeException(e);
452-
}
454+
try (PreparedStatement statement = connection.prepareStatement(sql)) {
455+
statement.setObject(1, context.holder().getId());
456+
statement.executeUpdate();
453457
}
458+
return;
454459
}
460+
persistentValueManager.insertInDatabase(connection, context.holder(), new ArrayList<>(context.initialPersistentValues().values()));
461+
cachedValueManager.setInRedis(jedis, new ArrayList<>(context.initialCachedValues().values()));
462+
}
455463

456-
deleteFromCache(holder, dataList);
457-
removeUniqueData(holder.getClass(), holder.getId());
458-
//todo: remove add, remove, and update handlers
459-
//todo: handle foreign PVs
460-
//todo: handle CVs
461-
//todo: handle PVCs
462-
//todo: handle PUDCs
463-
//todo: handle PMTMCs
464+
public void delete(UniqueData holder) {
465+
DeleteContext context = buildDeleteContext(holder);
466+
logger.trace("Deleting: {}", context);
467+
deleteFromCache(context);
468+
//todo: i really dislike that update handlers are called when things are deleted. revisit this
464469

465470
ThreadUtils.submit(() -> {
466471
try (Connection connection = getConnection();
467472
Jedis jedis = jedisProvider.getJedis()
468473
) {
469-
deleteFromDataSource(connection, jedis, holder);
474+
deleteFromDataSource(connection, jedis, context);
470475
} catch (SQLException e) {
471476
logger.error("Error deleting data", e);
472477
throw new RuntimeException(e);
473478
}
474479
});
475480
}
476481

477-
private void deleteFromCache(UniqueData holder, List<Data<?>> dataList) {
478-
for (Data<?> data : dataList) {
479-
if (data instanceof PersistentValue<?> value) {
480-
persistentValueManager.uncache(value);
481-
482-
//Uncache the id column as well
483-
persistentValueManager.uncache(
484-
value.getSchema(),
485-
value.getTable(),
486-
value.getIdColumn(),
487-
holder.getId(),
488-
value.getIdColumn()
489-
);
490-
} else if (data instanceof CachedValue<?> value) {
491-
cache.remove(value.getKey());
492-
} else if (data instanceof PersistentValueCollection<?> collection) {
493-
persistentCollectionManager.removeEntriesFromCache(collection, persistentCollectionManager.getCollectionEntries(collection));
494-
} else if (data instanceof PersistentUniqueDataCollection<?> collection) {
495-
persistentCollectionManager.removeEntriesFromInternalMap(collection, persistentCollectionManager.getCollectionEntries(collection));
482+
public void deleteSync(UniqueData holder) {
483+
DeleteContext context = buildDeleteContext(holder);
484+
logger.trace("Deleting: {}", context);
485+
deleteFromCache(context);
486+
487+
try (Connection connection = getConnection();
488+
Jedis jedis = jedisProvider.getJedis()
489+
) {
490+
deleteFromDataSource(connection, jedis, context);
491+
} catch (SQLException e) {
492+
logger.error("Error deleting data", e);
493+
throw new RuntimeException(e);
494+
}
495+
}
496+
497+
private DeleteContext buildDeleteContext(UniqueData holder) {
498+
Set<Data<?>> toDelete = new HashSet<>();
499+
Set<UniqueData> holders = new HashSet<>();
500+
extractDataToDelete(holder, holders, toDelete);
501+
Map<DataKey, Object> oldValues = new HashMap<>();
502+
for (Data<?> data : toDelete) {
503+
if (data instanceof PersistentValue<?> || data instanceof CachedValue<?>) {
504+
try {
505+
Object value = get(data.getKey());
506+
oldValues.put(data.getKey(), value == NULL_MARKER ? null : value);
507+
} catch (DataDoesNotExistException e) {
508+
// This is fine, it just means the value was null
509+
}
496510
}
497511
}
498512

513+
return new DeleteContext(holders, toDelete, oldValues);
499514
}
500515

501-
private void deleteFromDataSource(Connection connection, Jedis jedis, UniqueData holder) throws SQLException {
502-
String sql = "DELETE FROM " + holder.getSchema() + "." + holder.getTable() + " WHERE " + holder.getIdentifier().getColumn() + " = ?";
503-
logSQL(sql);
516+
private void extractDataToDelete(UniqueData holder, Set<UniqueData> holders, Set<Data<?>> toDelete) {
517+
if (holders.contains(holder)) {
518+
return;
519+
}
520+
holders.add(holder);
504521

505-
try (PreparedStatement statement = connection.prepareStatement(sql)) {
506-
statement.setObject(1, holder.getId());
507-
statement.execute();
522+
//Add the root holder's id column to the list of things to delete, just in case the holder is empty
523+
toDelete.add(PersistentValue.of(holder.getRootHolder(), UUID.class, holder.getRootHolder().getIdentifier().getColumn()));
524+
525+
for (Field field : ReflectionUtils.getFields(holder.getClass())) {
526+
field.setAccessible(true);
527+
528+
if (Data.class.isAssignableFrom(field.getType())) {
529+
try {
530+
Data<?> data = (Data<?>) field.get(holder);
531+
532+
//Always delete the backing value since it's in the same table as the holder
533+
if (data instanceof Reference<?> reference) {
534+
toDelete.add(reference.getBackingValue());
535+
}
536+
537+
if (data.getDeletionStrategy() == DeletionStrategy.NO_ACTION) {
538+
continue;
539+
}
540+
541+
if (data instanceof Reference<?> reference) {
542+
UUID id = reference.getForeignId();
543+
if (id != null) {
544+
UniqueData foreignData = reference.get();
545+
if (foreignData != null) {
546+
extractDataToDelete(foreignData, holders, toDelete);
547+
}
548+
}
549+
}
550+
551+
if (data instanceof PersistentUniqueDataCollection<?> collection) {
552+
if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) {
553+
for (UniqueData dataInCollection : collection) {
554+
extractDataToDelete(dataInCollection, holders, toDelete);
555+
}
556+
}
557+
}
558+
559+
if (data instanceof PersistentManyToManyCollection<?> collection) {
560+
if (collection.getDeletionStrategy() == DeletionStrategy.CASCADE) {
561+
for (UniqueData dataInCollection : collection) {
562+
extractDataToDelete(dataInCollection, holders, toDelete);
563+
}
564+
}
565+
}
566+
567+
toDelete.add(data);
568+
} catch (IllegalAccessException e) {
569+
throw new RuntimeException(e);
570+
}
571+
}
572+
}
573+
}
574+
575+
private void deleteFromCache(DeleteContext context) {
576+
persistentCollectionManager.deleteFromCache(context);
577+
persistentValueManager.deleteFromCache(context);
578+
cachedValueManager.deleteFromCache(context);
579+
580+
for (UniqueData holder : context.holders()) {
581+
removeUniqueData(holder.getClass(), holder.getId());
508582
}
509583
}
510584

585+
@Blocking
586+
private void deleteFromDataSource(Connection connection, Jedis jedis, DeleteContext context) throws SQLException {
587+
for (UniqueData holder : context.holders()) {
588+
String sql = "DELETE FROM " + holder.getSchema() + "." + holder.getTable() + " WHERE " + holder.getIdentifier().getColumn() + " = ?";
589+
logSQL(sql);
590+
591+
try (PreparedStatement statement = connection.prepareStatement(sql)) {
592+
statement.setObject(1, holder.getId());
593+
statement.executeUpdate();
594+
}
595+
}
596+
persistentValueManager.deleteFromDatabase(connection, context);
597+
persistentCollectionManager.deleteFromDatabase(connection, context);
598+
cachedValueManager.deleteFromRedis(jedis, context);
599+
}
600+
511601
public <T extends UniqueData> List<T> getAll(Class<T> clazz) {
512602
return uniqueDataIds.get(clazz).stream().map(id -> get(clazz, id)).toList();
513603
}
@@ -567,7 +657,13 @@ private List<Data<?>> extractDataDependencies(Class<? extends UniqueData> clazz)
567657
return dependencies;
568658
}
569659

660+
public synchronized void removeFromCacheIf(Predicate<DataKey> predicate) {
661+
Set<DataKey> keysToRemove = cache.keySet().stream().filter(predicate).collect(Collectors.toSet());
662+
keysToRemove.forEach(cache::remove);
663+
}
664+
570665
public void dump() {
666+
logger.debug("Dumping cache:");
571667
for (Map.Entry<DataKey, CacheEntry> entry : cache.entrySet()) {
572668
logger.debug("{} -> {}", entry.getKey(), entry.getValue().value());
573669
}
@@ -691,6 +787,10 @@ public <T> void cache(DataKey key, Class<?> valueDataType, T value, Instant inst
691787
}
692788
}
693789

790+
public int getCacheSize() {
791+
return cache.size();
792+
}
793+
694794
public void uncache(DataKey key) {
695795
Collection<ValueUpdateHandler<?>> updateHandlers = valueUpdateHandlers.get(key);
696796
CacheEntry existing = cache.get(key);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.staticstudios.data;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
public interface Deletable {
6+
Deletable deletionStrategy(DeletionStrategy strategy);
7+
8+
@NotNull DeletionStrategy getDeletionStrategy();
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.staticstudios.data;
2+
3+
import net.staticstudios.data.data.Data;
4+
import net.staticstudios.data.data.UniqueData;
5+
import net.staticstudios.data.key.DataKey;
6+
7+
import java.util.Map;
8+
import java.util.Set;
9+
10+
public record DeleteContext(Set<UniqueData> holders, Set<Data<?>> toDelete, Map<DataKey, Object> oldValues) {
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.staticstudios.data;
2+
3+
public enum DeletionStrategy {
4+
/**
5+
* When the parent holder is deleted, delete this data as well.
6+
*/
7+
CASCADE,
8+
/**
9+
* Do nothing when the parent holder is deleted.
10+
*/
11+
NO_ACTION,
12+
/**
13+
* This is only for use in PersistentCollections created via
14+
* {@link net.staticstudios.data.data.collection.PersistentCollection#oneToMany} or
15+
* {@link net.staticstudios.data.data.collection.PersistentCollection#manyToMany}
16+
*/
17+
UNLINK
18+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
package net.staticstudios.data;
22

3-
public record ValueUpdate<T>(T oldValue, T newValue) {
3+
import org.jetbrains.annotations.Nullable;
4+
5+
public record ValueUpdate<T>(@Nullable T oldValue, @Nullable T newValue) {
46
}

src/main/java/net/staticstudios/data/data/Data.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package net.staticstudios.data.data;
22

33
import net.staticstudios.data.DataManager;
4+
import net.staticstudios.data.Deletable;
45
import net.staticstudios.data.key.DataKey;
56

6-
public interface Data<T> {
7+
public interface Data<T> extends Deletable {
78

89
Class<T> getDataType();
910

0 commit comments

Comments
 (0)