Skip to content

Commit d3bc1b1

Browse files
Merge pull request #209 from xdev-software/delete-referenced-object
Delete referenced object
2 parents ddd62fa + 2899c4a commit d3bc1b1

File tree

9 files changed

+180
-16
lines changed

9 files changed

+180
-16
lines changed

assets/DependingClasses.drawio

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<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">
2+
<diagram name="Page-1" id="6GyipOatxIIVSVKHxFAy">
3+
<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">
4+
<root>
5+
<mxCell id="0" />
6+
<mxCell id="1" parent="0" />
7+
<mxCell id="hvzIIKVZ51MqT405g5E5-15" value="" style="rounded=0;whiteSpace=wrap;html=1;strokeColor=none;" vertex="1" parent="1">
8+
<mxGeometry x="150" y="120" width="430" height="250" as="geometry" />
9+
</mxCell>
10+
<mxCell id="hvzIIKVZ51MqT405g5E5-5" value="" style="group" vertex="1" connectable="0" parent="1">
11+
<mxGeometry x="440" y="140" width="100" height="100" as="geometry" />
12+
</mxCell>
13+
<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">
14+
<mxGeometry x="20" y="20" width="60" height="80" as="geometry" />
15+
</mxCell>
16+
<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">
17+
<mxGeometry y="-10" width="100" height="20" as="geometry" />
18+
</mxCell>
19+
<mxCell id="hvzIIKVZ51MqT405g5E5-6" value="" style="group" vertex="1" connectable="0" parent="1">
20+
<mxGeometry x="190" y="140" width="100" height="100" as="geometry" />
21+
</mxCell>
22+
<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">
23+
<mxGeometry x="20" y="20" width="60" height="80" as="geometry" />
24+
</mxCell>
25+
<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">
26+
<mxGeometry y="-10" width="100" height="20" as="geometry" />
27+
</mxCell>
28+
<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">
29+
<mxGeometry x="180" y="290" width="120" height="60" as="geometry" />
30+
</mxCell>
31+
<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">
32+
<mxGeometry x="430" y="290" width="120" height="60" as="geometry" />
33+
</mxCell>
34+
<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">
35+
<mxGeometry width="100" height="100" relative="1" as="geometry">
36+
<mxPoint x="320" y="470" as="sourcePoint" />
37+
<mxPoint x="480" y="470" as="targetPoint" />
38+
</mxGeometry>
39+
</mxCell>
40+
<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">
41+
<mxGeometry x="0.21" relative="1" as="geometry">
42+
<mxPoint x="-17" y="-30" as="offset" />
43+
</mxGeometry>
44+
</mxCell>
45+
</root>
46+
</mxGraphModel>
47+
</diagram>
48+
</mxfile>
Lines changed: 1 addition & 0 deletions
Loading

docs/modules/ROOT/pages/features/lazies.adoc

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,46 @@ import software.xdev.spring.data.eclipse.store.repository.lazy.SpringDataEclipse
2525
2626
public class Owner extends Person
2727
{
28-
private String address;
29-
28+
//...
3029
private final Lazy<List<Pet>> pets = SpringDataEclipseStoreLazy.build(new ArrayList<>());
3130
//...
3231
----
3332

33+
== FetchType.LAZY
34+
35+
In Spring JPA, lazy loading is achieved by annotating a field or property with ``FetchType.LAZY``.
36+
This approach leverages JPA's built-in mechanisms to defer the retrieval of the related entity until it is accessed.
37+
38+
In contrast, {product-name} takes a different approach.
39+
Instead of using annotations, you wrap the object intended to be loaded lazily in a ``Lazy``-wrapper.
40+
This wrapper encapsulates the object and ensures it is only loaded when needed.
41+
42+
[source,java,title="JPA Example with lazy"]
43+
----
44+
import jakarta.persistence.OneToMany;
45+
46+
public class Owner extends Person
47+
{
48+
@OneToMany(fetch = FetchType.LAZY)
49+
private final List<Pet> pets = new ArrayList<>();
50+
//...
51+
----
52+
53+
[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]"]
54+
----
55+
package software.xdev.spring.data.eclipse.store.demo.complex.owner;
56+
//...
57+
import software.xdev.spring.data.eclipse.store.repository.lazy.SpringDataEclipseStoreLazy;
58+
59+
public class Owner extends Person
60+
{
61+
private final List<Lazy<Pet>> pets = new ArrayList<>();
62+
//...
63+
----
64+
65+
The ``Lazy``-wrapper makes lazy loading **explicit and flexible**, avoiding JPA-specific overhead and potential exceptions.
66+
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.
67+
3468
== Repositories
3569

3670
Entities in a repository are by default **not lazy**.

docs/modules/ROOT/pages/known-issues.adoc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,37 @@ This **should** handle most problems with the ClassLoader.
2929
Restarting the storage leads to a reloading of all entities and may take some time, yet circumvents the Restart ClassLoader Issue.
3030

3131
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].
32+
33+
== Multiple Repositories with related entities [[multi-repos-with-related-entities]]
34+
35+
In SQL databases, relationships between entities are explicitly defined and enforced by the database system.
36+
This ensures that constraints, such as foreign keys, are consistently maintained.
37+
38+
In contrast, relationships in a Java object graph are solely defined by the developer.
39+
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.
40+
Consequently, finding such a relationship requires searching the entire object graph, which can be highly inefficient.
41+
42+
image::DependingClasses.svg[Example structure with orders and articles]
43+
44+
Example Scenario:
45+
Consider an *order object* that contains references to several *article objects*.
46+
In this case, determining which order contains a specific article is nearly impossible without traversing the entire object graph to locate it.
47+
This lack of direct reference contrasts sharply with the behavior of SQL databases.
48+
49+
What Happens When an Article is Deleted?
50+
51+
1. In an *SQL Database*: +
52+
Attempting to delete an article that is still referenced (e.g., by an order) would typically result in an exception. +
53+
The database enforces referential integrity, preventing the deletion of a referenced entity.
54+
55+
2. In *{product-name}*: +
56+
Deleting an article from the article repository is allowed, even if it is still referenced elsewhere. +
57+
The system does not track or enforce such references.
58+
As a result:
59+
60+
* The article is removed from the repository.
61+
* However, the order still retains its reference to the now-deleted article.
62+
* If the order is subsequently saved, the article is reintroduced into the repository.
63+
64+
This behavior is fundamentally different from the strict relationship management seen in SQL databases.
65+
Developers must be aware of these differences to avoid unintended side effects in their applications.

spring-data-eclipse-store/src/main/java/software/xdev/spring/data/eclipse/store/repository/EclipseStoreStorage.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ public <T> void store(
289289
final Collection<Object> entitiesAndPossiblyNonEntitiesToStore =
290290
this.collectRootEntitiesToStore(
291291
this.getEntityData(clazz),
292-
clazz,
293292
entitiesToStore);
294293
entitiesAndPossiblyNonEntitiesToStore.addAll(nonEntitiesToStore);
295294
if(LOG.isDebugEnabled())
@@ -312,7 +311,6 @@ public <T> void store(
312311
*/
313312
private <T, ID> Collection<Object> collectRootEntitiesToStore(
314313
final EntityData<T, ID> entityData,
315-
final Class<T> clazz,
316314
final Iterable<T> entitiesToStore)
317315
{
318316
final Collection<Object> objectsToStore = new ArrayList<>();
@@ -337,6 +335,7 @@ public <T> void delete(final Class<T> clazz, final T entityToRemove)
337335
{
338336
final EntityData<T, ?> entityData = this.getEntityData(clazz);
339337
this.storageManager.storeAll(entityData.removeEntityAndReturnObjectsToStore(entityToRemove));
338+
340339
if(LOG.isDebugEnabled())
341340
{
342341
LOG.debug("Deleted single entity of class {}.", clazz.getSimpleName());

spring-data-eclipse-store/src/main/java/software/xdev/spring/data/eclipse/store/repository/support/reposyncer/SimpleRepositorySynchronizer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public SimpleRepositorySynchronizer(final RootDataV2_4 root)
5050
return;
5151
}
5252
final Class<Object> objectInGraphClass = (Class<Object>)objectInGraph.getClass();
53-
EntityData<Object, Object> entityDataForCurrentObject =
53+
final EntityData<Object, Object> entityDataForCurrentObject =
5454
this.root.getEntityData(objectInGraphClass);
5555
if(entityDataForCurrentObject != null
5656
&& !entityDataForCurrentObject.containsEntity(objectInGraph))
@@ -62,6 +62,9 @@ public SimpleRepositorySynchronizer(final RootDataV2_4 root)
6262
).buildObjectGraphTraverser();
6363
}
6464

65+
/**
66+
* This method can take a huge amount of time. Must be watched closely.
67+
*/
6568
@Override
6669
public Collection<EntityData<Object, Object>> syncAndReturnChangedObjectLists(final Object objectToStore)
6770
{

spring-data-eclipse-store/src/test/java/software/xdev/spring/data/eclipse/store/integration/isolated/tests/deletion/DeletionTest.java

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,68 @@ void deleteReferencedObject(
5151
final ReferencingDaoObject referencingDaoObject = new ReferencingDaoObject(referencedObject);
5252
referencingRepository.save(referencingDaoObject);
5353

54-
Assertions.assertEquals(1, TestUtil.iterableToList(referencingRepository.findAll()).size());
55-
Assertions.assertEquals(1, TestUtil.iterableToList(referencedRepository.findAll()).size());
54+
Assertions.assertEquals(1, referencingRepository.findAll().size());
55+
Assertions.assertEquals(1, referencedRepository.findAll().size());
5656

5757
referencedRepository.delete(referencedObject);
5858

5959
TestUtil.doBeforeAndAfterRestartOfDatastore(
6060
this.configuration,
6161
() -> {
62-
Assertions.assertTrue(TestUtil.iterableToList(referencedRepository.findAll()).isEmpty());
63-
final List<ReferencingDaoObject> referencingDaoObjects =
64-
TestUtil.iterableToList(referencingRepository.findAll());
62+
Assertions.assertTrue(referencedRepository.findAll().isEmpty());
63+
final List<ReferencingDaoObject> referencingDaoObjects = referencingRepository.findAll();
6564
Assertions.assertEquals(1, referencingDaoObjects.size());
6665
Assertions.assertNotNull(referencingDaoObjects.get(0).getValue());
6766
}
6867
);
6968
}
7069

70+
@Test
71+
void deleteUnreferencedObject(@Autowired final ReferencedRepository referencedRepository)
72+
{
73+
final ReferencedDaoObject referencedObject = new ReferencedDaoObject("someValue");
74+
referencedRepository.save(referencedObject);
75+
76+
Assertions.assertEquals(1, referencedRepository.findAll().size());
77+
78+
referencedRepository.delete(referencedObject);
79+
80+
TestUtil.doBeforeAndAfterRestartOfDatastore(
81+
this.configuration,
82+
() -> {
83+
Assertions.assertTrue(referencedRepository.findAll().isEmpty());
84+
}
85+
);
86+
}
87+
88+
@Test
89+
void deleteDoubleReferencedObject(
90+
@Autowired final ReferencedRepository referencedRepository,
91+
@Autowired final ReferencingRepository referencingRepository)
92+
{
93+
final ReferencedDaoObject referencedObject = new ReferencedDaoObject("someValue");
94+
final ReferencingDaoObject referencingDaoObject1 = new ReferencingDaoObject(referencedObject);
95+
final ReferencingDaoObject referencingDaoObject2 = new ReferencingDaoObject(referencedObject);
96+
referencingRepository.save(referencingDaoObject1);
97+
referencingRepository.save(referencingDaoObject2);
98+
99+
Assertions.assertEquals(2, referencingRepository.findAll().size());
100+
Assertions.assertEquals(1, referencedRepository.findAll().size());
101+
102+
referencedRepository.delete(referencedObject);
103+
104+
TestUtil.doBeforeAndAfterRestartOfDatastore(
105+
this.configuration,
106+
() -> {
107+
Assertions.assertTrue(referencedRepository.findAll().isEmpty());
108+
final List<ReferencingDaoObject> referencingDaoObjects = referencingRepository.findAll();
109+
Assertions.assertEquals(2, referencingDaoObjects.size());
110+
Assertions.assertNotNull(referencingDaoObjects.get(0).getValue());
111+
Assertions.assertNotNull(referencingDaoObjects.get(1).getValue());
112+
}
113+
);
114+
}
115+
71116
@Test
72117
void restoreDeletedReferencedObject(
73118
@Autowired final ReferencedRepository referencedRepository,
@@ -84,8 +129,8 @@ void restoreDeletedReferencedObject(
84129
TestUtil.doBeforeAndAfterRestartOfDatastore(
85130
this.configuration,
86131
() -> {
87-
Assertions.assertEquals(1, TestUtil.iterableToList(referencingRepository.findAll()).size());
88-
Assertions.assertEquals(1, TestUtil.iterableToList(referencedRepository.findAll()).size());
132+
Assertions.assertEquals(1, referencingRepository.findAll().size());
133+
Assertions.assertEquals(1, referencedRepository.findAll().size());
89134
}
90135
);
91136
}

0 commit comments

Comments
 (0)