Skip to content

Delete referenced object #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions assets/DependingClasses.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" version="25.0.3">
<diagram name="Page-1" id="6GyipOatxIIVSVKHxFAy">
<mxGraphModel dx="1562" dy="845" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="hvzIIKVZ51MqT405g5E5-15" value="" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=none;" vertex="1" parent="1">
<mxGeometry x="150" y="120" width="430" height="250" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-5" value="" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="440" y="140" width="100" height="100" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-3" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=none;strokeColor=#D62125;strokeWidth=3;align=center;verticalAlign=middle;fontFamily=Helvetica;fontSize=12;fontColor=default;" vertex="1" parent="hvzIIKVZ51MqT405g5E5-5">
<mxGeometry x="20" y="20" width="60" height="80" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-4" value="&lt;span style=&quot;font-weight: 400; text-wrap-mode: wrap;&quot;&gt;ArticleRepository&lt;/span&gt;" style="text;strokeColor=none;fillColor=none;html=1;fontSize=15;fontStyle=1;verticalAlign=middle;align=center;" vertex="1" parent="hvzIIKVZ51MqT405g5E5-5">
<mxGeometry y="-10" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-6" value="" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="190" y="140" width="100" height="100" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-1" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=none;strokeColor=#D62125;strokeWidth=3;" vertex="1" parent="hvzIIKVZ51MqT405g5E5-6">
<mxGeometry x="20" y="20" width="60" height="80" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-2" value="&lt;span style=&quot;font-weight: 400; text-wrap-mode: wrap;&quot;&gt;OrderRepository&lt;/span&gt;" style="text;strokeColor=none;fillColor=none;html=1;fontSize=15;fontStyle=1;verticalAlign=middle;align=center;" vertex="1" parent="hvzIIKVZ51MqT405g5E5-6">
<mxGeometry y="-10" width="100" height="20" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-7" value="&lt;font&gt;Order&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=15;fillColor=none;strokeColor=#D62125;strokeWidth=3;align=center;verticalAlign=middle;fontFamily=Helvetica;fontColor=default;" vertex="1" parent="1">
<mxGeometry x="180" y="290" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-8" value="&lt;font&gt;Article&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=15;fillColor=none;strokeColor=#D62125;strokeWidth=3;align=center;verticalAlign=middle;fontFamily=Helvetica;fontColor=default;" vertex="1" parent="1">
<mxGeometry x="430" y="290" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-16" value="" style="shape=flexArrow;endArrow=classic;startArrow=none;html=1;rounded=0;strokeColor=#C91F24;strokeWidth=2;fillColor=default;startFill=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="hvzIIKVZ51MqT405g5E5-7" target="hvzIIKVZ51MqT405g5E5-8">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="320" y="470" as="sourcePoint" />
<mxPoint x="480" y="470" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="hvzIIKVZ51MqT405g5E5-20" value="&lt;span style=&quot;font-family: &amp;quot;Courier New&amp;quot;; font-size: 15px; font-weight: 700;&quot;&gt;@OneToMany&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="hvzIIKVZ51MqT405g5E5-16">
<mxGeometry x="0.21" relative="1" as="geometry">
<mxPoint x="-17" y="-30" as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
1 change: 1 addition & 0 deletions docs/modules/ROOT/assets/images/DependingClasses.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 36 additions & 2 deletions docs/modules/ROOT/pages/features/lazies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,46 @@ import software.xdev.spring.data.eclipse.store.repository.lazy.SpringDataEclipse

public class Owner extends Person
{
private String address;

//...
private final Lazy<List<Pet>> pets = SpringDataEclipseStoreLazy.build(new ArrayList<>());
//...
----

== FetchType.LAZY

In Spring JPA, lazy loading is achieved by annotating a field or property with ``FetchType.LAZY``.
This approach leverages JPA's built-in mechanisms to defer the retrieval of the related entity until it is accessed.

In contrast, {product-name} takes a different approach.
Instead of using annotations, you wrap the object intended to be loaded lazily in a ``Lazy``-wrapper.
This wrapper encapsulates the object and ensures it is only loaded when needed.

[source,java,title="JPA Example with lazy"]
----
import jakarta.persistence.OneToMany;

public class Owner extends Person
{
@OneToMany(fetch = FetchType.LAZY)
private final List<Pet> pets = new ArrayList<>();
//...
----

[source,java,title="https://github.com/xdev-software/spring-data-eclipse-store/tree/develop/spring-data-eclipse-store-demo/src/main/java/software/xdev/spring/data/eclipse/store/demo/complex/owner/Owner.java[Slightly changed example from complex demo]"]
----
package software.xdev.spring.data.eclipse.store.demo.complex.owner;
//...
import software.xdev.spring.data.eclipse.store.repository.lazy.SpringDataEclipseStoreLazy;

public class Owner extends Person
{
private final List<Lazy<Pet>> pets = new ArrayList<>();
//...
----

The ``Lazy``-wrapper makes lazy loading **explicit and flexible**, avoiding JPA-specific overhead and potential exceptions.
But it introduces a custom, less-standardized approach that may increase boilerplate and requires developers to remember to use the wrapper, which could lead to errors if overlooked.

== Repositories

Entities in a repository are by default **not lazy**.
Expand Down
34 changes: 34 additions & 0 deletions docs/modules/ROOT/pages/known-issues.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,37 @@ This **should** handle most problems with the ClassLoader.
Restarting the storage leads to a reloading of all entities and may take some time, yet circumvents the Restart ClassLoader Issue.

The behavior can be configured through xref:configuration.adoc#context-close-shutdown-storage[Properties] and is implemented in the https://github.com/xdev-software/spring-data-eclipse-store/tree/develop/spring-data-eclipse-store/src/main/java/software/xdev/spring/data/eclipse/store/repository/config/EclipseStoreClientConfiguration.java[EclipseStoreClientConfiguration.java].

== Multiple Repositories with related entities [[multi-repos-with-related-entities]]

In SQL databases, relationships between entities are explicitly defined and enforced by the database system.
This ensures that constraints, such as foreign keys, are consistently maintained.

In contrast, relationships in a Java object graph are solely defined by the developer.
If an object within the graph does not maintain a reference back to its parent or containing object, it has no inherent knowledge of that relationship.
Consequently, finding such a relationship requires searching the entire object graph, which can be highly inefficient.

image::DependingClasses.svg[Example structure with orders and articles]

Example Scenario:
Consider an *order object* that contains references to several *article objects*.
In this case, determining which order contains a specific article is nearly impossible without traversing the entire object graph to locate it.
This lack of direct reference contrasts sharply with the behavior of SQL databases.

What Happens When an Article is Deleted?

1. In an *SQL Database*: +
Attempting to delete an article that is still referenced (e.g., by an order) would typically result in an exception. +
The database enforces referential integrity, preventing the deletion of a referenced entity.

2. In *{product-name}*: +
Deleting an article from the article repository is allowed, even if it is still referenced elsewhere. +
The system does not track or enforce such references.
As a result:

* The article is removed from the repository.
* However, the order still retains its reference to the now-deleted article.
* If the order is subsequently saved, the article is reintroduced into the repository.

This behavior is fundamentally different from the strict relationship management seen in SQL databases.
Developers must be aware of these differences to avoid unintended side effects in their applications.
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,6 @@ public <T> void store(
final Collection<Object> entitiesAndPossiblyNonEntitiesToStore =
this.collectRootEntitiesToStore(
this.getEntityData(clazz),
clazz,
entitiesToStore);
entitiesAndPossiblyNonEntitiesToStore.addAll(nonEntitiesToStore);
if(LOG.isDebugEnabled())
Expand All @@ -312,7 +311,6 @@ public <T> void store(
*/
private <T, ID> Collection<Object> collectRootEntitiesToStore(
final EntityData<T, ID> entityData,
final Class<T> clazz,
final Iterable<T> entitiesToStore)
{
final Collection<Object> objectsToStore = new ArrayList<>();
Expand All @@ -337,6 +335,7 @@ public <T> void delete(final Class<T> clazz, final T entityToRemove)
{
final EntityData<T, ?> entityData = this.getEntityData(clazz);
this.storageManager.storeAll(entityData.removeEntityAndReturnObjectsToStore(entityToRemove));

if(LOG.isDebugEnabled())
{
LOG.debug("Deleted single entity of class {}.", clazz.getSimpleName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public SimpleRepositorySynchronizer(final RootDataV2_4 root)
return;
}
final Class<Object> objectInGraphClass = (Class<Object>)objectInGraph.getClass();
EntityData<Object, Object> entityDataForCurrentObject =
final EntityData<Object, Object> entityDataForCurrentObject =
this.root.getEntityData(objectInGraphClass);
if(entityDataForCurrentObject != null
&& !entityDataForCurrentObject.containsEntity(objectInGraph))
Expand All @@ -62,6 +62,9 @@ public SimpleRepositorySynchronizer(final RootDataV2_4 root)
).buildObjectGraphTraverser();
}

/**
* This method can take a huge amount of time. Must be watched closely.
*/
@Override
public Collection<EntityData<Object, Object>> syncAndReturnChangedObjectLists(final Object objectToStore)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,68 @@ void deleteReferencedObject(
final ReferencingDaoObject referencingDaoObject = new ReferencingDaoObject(referencedObject);
referencingRepository.save(referencingDaoObject);

Assertions.assertEquals(1, TestUtil.iterableToList(referencingRepository.findAll()).size());
Assertions.assertEquals(1, TestUtil.iterableToList(referencedRepository.findAll()).size());
Assertions.assertEquals(1, referencingRepository.findAll().size());
Assertions.assertEquals(1, referencedRepository.findAll().size());

referencedRepository.delete(referencedObject);

TestUtil.doBeforeAndAfterRestartOfDatastore(
this.configuration,
() -> {
Assertions.assertTrue(TestUtil.iterableToList(referencedRepository.findAll()).isEmpty());
final List<ReferencingDaoObject> referencingDaoObjects =
TestUtil.iterableToList(referencingRepository.findAll());
Assertions.assertTrue(referencedRepository.findAll().isEmpty());
final List<ReferencingDaoObject> referencingDaoObjects = referencingRepository.findAll();
Assertions.assertEquals(1, referencingDaoObjects.size());
Assertions.assertNotNull(referencingDaoObjects.get(0).getValue());
}
);
}

@Test
void deleteUnreferencedObject(@Autowired final ReferencedRepository referencedRepository)
{
final ReferencedDaoObject referencedObject = new ReferencedDaoObject("someValue");
referencedRepository.save(referencedObject);

Assertions.assertEquals(1, referencedRepository.findAll().size());

referencedRepository.delete(referencedObject);

TestUtil.doBeforeAndAfterRestartOfDatastore(
this.configuration,
() -> {
Assertions.assertTrue(referencedRepository.findAll().isEmpty());
}
);
}

@Test
void deleteDoubleReferencedObject(
@Autowired final ReferencedRepository referencedRepository,
@Autowired final ReferencingRepository referencingRepository)
{
final ReferencedDaoObject referencedObject = new ReferencedDaoObject("someValue");
final ReferencingDaoObject referencingDaoObject1 = new ReferencingDaoObject(referencedObject);
final ReferencingDaoObject referencingDaoObject2 = new ReferencingDaoObject(referencedObject);
referencingRepository.save(referencingDaoObject1);
referencingRepository.save(referencingDaoObject2);

Assertions.assertEquals(2, referencingRepository.findAll().size());
Assertions.assertEquals(1, referencedRepository.findAll().size());

referencedRepository.delete(referencedObject);

TestUtil.doBeforeAndAfterRestartOfDatastore(
this.configuration,
() -> {
Assertions.assertTrue(referencedRepository.findAll().isEmpty());
final List<ReferencingDaoObject> referencingDaoObjects = referencingRepository.findAll();
Assertions.assertEquals(2, referencingDaoObjects.size());
Assertions.assertNotNull(referencingDaoObjects.get(0).getValue());
Assertions.assertNotNull(referencingDaoObjects.get(1).getValue());
}
);
}

@Test
void restoreDeletedReferencedObject(
@Autowired final ReferencedRepository referencedRepository,
Expand All @@ -84,8 +129,8 @@ void restoreDeletedReferencedObject(
TestUtil.doBeforeAndAfterRestartOfDatastore(
this.configuration,
() -> {
Assertions.assertEquals(1, TestUtil.iterableToList(referencingRepository.findAll()).size());
Assertions.assertEquals(1, TestUtil.iterableToList(referencedRepository.findAll()).size());
Assertions.assertEquals(1, referencingRepository.findAll().size());
Assertions.assertEquals(1, referencedRepository.findAll().size());
}
);
}
Expand Down
Loading
Loading