Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,17 @@ public interface EntityRepository<E extends Entity<ID>, ID> extends Repository {
/**
* Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key.
*
* <p>This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual
* record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, which will
* trigger a new database call. This approach is particularly useful when you need to minimize memory usage while
* keeping the option to re-fetch the complete record later.</p>
* <p>This method discards the full entity data and returns an attached ref that encapsulates just the primary key.
* The actual record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()},
* which will trigger a new database call. This approach is particularly useful when you need to minimize memory
* usage while keeping the option to re-fetch the complete record later.</p>
*
* <p>Unlike {@link Ref#unload()}, which returns a detached ref, this method returns an attached ref that can
* re-fetch the entity from the database.</p>
*
* @param entity the entity to unload into a lightweight ref.
* @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when
* needed.
* @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched
* again when needed.
* @since 1.3
*/
Ref<E> unload(@Nonnull E entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,14 +506,16 @@ public Ref<E> ref(@Nonnull E entity) {
/**
* Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key.
*
* <p>This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual
* record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, which will
* trigger a new database call. This approach is particularly useful when you need to minimize memory usage while
* keeping the option to re-fetch the complete record later.</p>
* <p>This method discards the full entity data and returns an attached ref that encapsulates just the primary key.
* The actual record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()},
* which will trigger a new database call.</p>
*
* <p>Unlike {@link Ref#unload()}, which returns a detached ref, this method returns an attached ref that can
* re-fetch the entity from the database.</p>
*
* @param entity the entity to unload into a lightweight ref.
* @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when
* needed.
* @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched
* again when needed.
* @since 1.3
*/
@Override
Expand Down
6 changes: 1 addition & 5 deletions storm-core/src/main/java/st/orm/core/spi/RefFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,7 @@ public <T extends Data, ID> Ref<T> create(@Nonnull Class<T> type, @Nonnull ID pk
@Override
public <T extends Data, ID> Ref<T> create(@Nonnull T record, @Nonnull ID pk) {
var type = (Class<T>) record.getClass();
var supplier = new LazySupplier<>(() ->
((QueryBuilder<T, T, ID>) template
.selectFrom(type))
.where(pk)
.getSingleResult(), record);
var supplier = new LazySupplier<>(record);
return create(supplier, type, pk);
}

Expand Down
21 changes: 7 additions & 14 deletions storm-core/src/main/java/st/orm/core/spi/RefImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import st.orm.Data;
import st.orm.Entity;
import st.orm.Ref;
import st.orm.core.repository.EntityRepository;
import st.orm.core.template.impl.LazySupplier;

/**
Expand Down Expand Up @@ -107,23 +109,14 @@ public boolean isFetchable() {
}

/**
* Returns a ref with the same identity but without data.
* Returns a detached ref with the same identity but without data. The returned ref is not attached to a database
* context. To obtain an attached ref that can re-fetch the record, use
* {@link EntityRepository#unload(Entity) EntityRepository.unload()} instead.
*
* <p>For attached refs, this clears the fetched record while preserving the ability to re-fetch from the database.
* The same ref instance may be returned since the data can be recovered on demand.</p>
*
* <p>For detached refs that hold a loaded record, a new unloaded ref is returned. Note that calling {@link #fetch()}
* on the returned ref will fail since there is no database connection to recover the data. Use this with caution
* when working with detached refs, as the original data cannot be retrieved.</p>
*
* <p>For detached refs that are already unloaded, this method returns the same instance.</p>
*
* @return a ref with the same type and primary key but without cached data; may return {@code this} if no new
* instance is required.
* @return a detached ref with the same type and primary key but without cached data.
*/
@Override
public Ref<T> unload() {
supplier.remove();
return this;
return Ref.of(type, pk);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import java.util.function.Supplier;

/**
* Returns a lazily initialized value.
* Returns a lazily initialized value. Once the value has been resolved, the supplier is released for garbage
* collection.
*
* @param <T> the type of results supplied by this supplier.
*/
Expand All @@ -41,27 +42,41 @@ public static <T> Supplier<T> lazy(@Nonnull Supplier<T> supplier) {
return new LazySupplier<>(supplier);
}

private final Supplier<T> supplier;
private volatile Supplier<T> supplier;
private final AtomicReference<T> reference;

/**
* Creates a lazy supplier that will invoke the given supplier on the first call to {@link #get()}.
*
* @param supplier the supplier that provides the value.
*/
public LazySupplier(@Nonnull Supplier<T> supplier) {
this.supplier = requireNonNull(supplier);
this.reference = new AtomicReference<>();
}

public LazySupplier(@Nonnull Supplier<T> supplier, @Nonnull T initialValue) {
this.supplier = requireNonNull(supplier);
/**
* Creates a lazy supplier with a pre-loaded initial value. The supplier is not retained and {@link #get()} will
* return the initial value without invoking any supplier.
*
* @param initialValue the pre-loaded value.
*/
public LazySupplier(@Nonnull T initialValue) {
this.supplier = null;
this.reference = new AtomicReference<>(requireNonNull(initialValue));
}

/**
* Gets a result.
* Gets a result. On the first invocation, the supplier is called to produce the value. The supplier is then
* released for garbage collection.
*
* @return a result
* @return a result.
*/
@Override
public T get() {
return reference.updateAndGet(value -> requireNonNullElseGet(value, supplier));
T result = reference.updateAndGet(value -> requireNonNullElseGet(value, supplier));
supplier = null; // Release the supplier (and its captured context) for GC.
return result;
}

/**
Expand All @@ -73,13 +88,6 @@ public Optional<T> value() {
return ofNullable(reference.get());
}

/**
* Removes the lazily loaded value. This will not remove the supplier.
*/
public void remove() {
reference.set(null);
}

/**
* Returns the first argument if it is non-{@code null} and otherwise returns the non-{@code null} value of
* {@code supplier.get()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,10 @@ private static <T extends Data, E> Metamodel<T, E> getModel(@Nonnull Class<T> ro
@Nonnull String path
) {
try {
Metamodel<T, ?> current = (Metamodel<T, ?>) Class.forName(rootTable.getName() + "Metamodel", true, rootTable.getClassLoader())
.getConstructor()
.newInstance();
Metamodel<T, ?> current = (Metamodel<T, ?>) Class.forName(
rootTable.getName() + "Metamodel", true, rootTable.getClassLoader())
.getMethod("instance")
.invoke(null);
for (String segment : path.split("\\.")) {
current = (Metamodel<T, ?>) readSegment(current, segment);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,11 @@ public void testLazyStaticFactoryMethod() {

@Test
public void testConstructorWithInitialValue() {
AtomicInteger callCount = new AtomicInteger(0);
LazySupplier<String> lazy = new LazySupplier<>(() -> {
callCount.incrementAndGet();
return "fallback";
}, "initial");
LazySupplier<String> lazy = new LazySupplier<>("initial");

assertEquals("initial", lazy.get());
assertEquals(0, callCount.get(), "Supplier should not be called when initial value is provided");
assertTrue(lazy.value().isPresent());
assertEquals("initial", lazy.value().get());
}

@Test
Expand All @@ -69,35 +66,25 @@ public void testValueReturnsPresentAfterGet() {

@Test
public void testValueReturnsPresentWithInitialValue() {
LazySupplier<String> lazy = new LazySupplier<>(() -> "fallback", "initial");
LazySupplier<String> lazy = new LazySupplier<>("initial");
assertTrue(lazy.value().isPresent(), "Value should be present when initial value is provided");
assertEquals("initial", lazy.value().get());
}

@Test
public void testRemoveClearsLazyValue() {
LazySupplier<String> lazy = new LazySupplier<>(() -> "hello");
lazy.get();
assertTrue(lazy.value().isPresent());

lazy.remove();
assertTrue(lazy.value().isEmpty(), "Value should be empty after remove()");
}

@Test
public void testGetAfterRemoveReinvokesSupplier() {
public void testSupplierReleasedAfterGet() {
AtomicInteger callCount = new AtomicInteger(0);
LazySupplier<String> lazy = new LazySupplier<>(() -> {
callCount.incrementAndGet();
return "hello";
});

lazy.get();
assertEquals(1, callCount.get());
assertEquals("hello", lazy.get());
assertEquals(1, callCount.get(), "Supplier should be called exactly once");

lazy.remove();
lazy.get();
assertEquals(2, callCount.get(), "After remove, get should invoke supplier again");
// After get(), the supplier should be released. Calling get() again returns the cached value.
assertEquals("hello", lazy.get());
assertEquals(1, callCount.get(), "Supplier should not be called again after first get()");
}

@Test
Expand Down
15 changes: 3 additions & 12 deletions storm-foundation/src/main/java/st/orm/DetachedRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,10 @@ public boolean isFetchable() {
}

/**
* Returns a ref with the same identity but without data.
* Returns a detached ref with the same identity but without data. Since this ref is already detached and unloaded,
* the same instance is returned.
*
* <p>For attached refs, this clears the fetched record while preserving the ability to re-fetch from the database.
* The same ref instance may be returned since the data can be recovered on demand.</p>
*
* <p>For detached refs that hold a loaded record, a new unloaded ref is returned. Note that calling {@link #fetch()}
* on the returned ref will fail since there is no database connection to recover the data. Use this with caution
* when working with detached refs, as the original data cannot be retrieved.</p>
*
* <p>For detached refs that are already unloaded, this method returns the same instance.</p>
*
* @return a ref with the same type and primary key but without cached data; may return {@code this} if no new
* instance is required.
* @return this instance.
*/
@Override
public Ref<T> unload() {
Expand Down
15 changes: 5 additions & 10 deletions storm-foundation/src/main/java/st/orm/Ref.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,19 +269,14 @@ default boolean isLoaded() {
}

/**
* Returns a ref with the same identity but without data.
* Returns a detached ref with the same identity but without data.
*
* <p>For attached refs, this clears the fetched record while preserving the ability to re-fetch from the database.
* The same ref instance may be returned since the data can be recovered on demand.</p>
* <p>The returned ref is not attached to a database context. Calling {@link #fetch()} on the returned ref will
* fail since there is no database connection to recover the data.</p>
*
* <p>For detached refs that hold a loaded record, a new unloaded ref is returned. Note that calling {@link #fetch()}
* on the returned ref will fail since there is no database connection to recover the data. Use this with caution
* when working with detached refs, as the original data cannot be retrieved.</p>
* <p>For refs that are already detached and unloaded, this method may return the same instance.</p>
*
* <p>For detached refs that are already unloaded, this method returns the same instance.</p>
*
* @return a ref with the same type and primary key but without cached data; may return {@code this} if no new
* instance is required.
* @return a detached ref with the same type and primary key but without cached data.
*/
Ref<T> unload();
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,17 @@ public interface EntityRepository<E extends Entity<ID>, ID> extends Repository {
/**
* Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key.
*
* <p>This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual
* record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()}, which will
* trigger a new database call. This approach is particularly useful when you need to minimize memory usage while
* keeping the option to re-fetch the complete record later.</p>
* <p>This method discards the full entity data and returns an attached ref that encapsulates just the primary key.
* The actual record is not retained in memory, but can be retrieved on demand by calling {@link Ref#fetch()},
* which will trigger a new database call. This approach is particularly useful when you need to minimize memory
* usage while keeping the option to re-fetch the complete record later.</p>
*
* <p>Unlike {@link Ref#unload()}, which returns a detached ref, this method returns an attached ref that can
* re-fetch the entity from the database.</p>
*
* @param entity the entity to unload into a lightweight ref.
* @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when
* needed.
* @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched
* again when needed.
* @since 1.3
*/
Ref<E> unload(@Nonnull E entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,17 @@ interface EntityRepository<E, ID : Any> : Repository where E : Entity<ID> {
* Unloads the given entity from memory by converting it into a lightweight ref containing only its primary key.
*
*
* This method discards the full entity data and returns a ref that encapsulates just the primary key. The actual
* record is not retained in memory, but can be retrieved on demand by calling [Ref.fetch], which will
* trigger a new database call. This approach is particularly useful when you need to minimize memory usage while
* keeping the option to re-fetch the complete record later.
* This method discards the full entity data and returns an attached ref that encapsulates just the primary key.
* The actual record is not retained in memory, but can be retrieved on demand by calling [Ref.fetch],
* which will trigger a new database call. This approach is particularly useful when you need to minimize memory
* usage while keeping the option to re-fetch the complete record later.
*
* Unlike [Ref.unload], which returns a detached ref, this method returns an attached ref that can
* re-fetch the entity from the database.
*
* @param entity the entity to unload into a lightweight ref.
* @return a ref containing only the primary key of the entity, allowing the full record to be fetched again when
* needed.
* @return an attached ref containing only the primary key of the entity, allowing the full record to be fetched
* again when needed.
* @since 1.3
*/
fun unload(entity: E): Ref<E>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ class MetamodelProcessor(
): String {
val builder = StringBuilder()
val className = classDeclaration.simpleName.asString()
val modelRef = "${className}Metamodel.instance<$className>()"
getModelProperties(classDeclaration).forEach { prop ->
val fieldName = prop.simpleName.asString()
val typeRef = prop.type
Expand All @@ -662,7 +663,7 @@ class MetamodelProcessor(
builder.append(" /** Represents the $className.$fieldName record. */\n")
builder.append(
" val $fieldName: $childMetaType = " +
"${className}Metamodel<$className>().$fieldName\n",
"$modelRef.$fieldName\n",
)
} else {
val kotlinTypeName = getKotlinTypeName(typeRef, packageName) // E (unwrap Ref)
Expand All @@ -672,7 +673,7 @@ class MetamodelProcessor(
builder.append(" /** Represents the $className.$fieldName field. */\n")
builder.append(
" val $fieldName: $baseClass<$className, $kotlinTypeName, $valueKotlinTypeName> = " +
"${className}Metamodel<$className>().$fieldName\n",
"$modelRef.$fieldName\n",
)
}
}
Expand Down Expand Up @@ -1093,6 +1094,20 @@ class MetamodelProcessor(
|${initClassFields(classDeclaration, packageName, metaClassName, forceNullableChain)}
| }
|$convenienceCtors
|${
if (isDataRoot && !forceNullableChain) {
"""
| companion object {
| private val INSTANCE: $metaClassName<*> = $metaClassName<st.orm.Data>()
|
| @Suppress("UNCHECKED_CAST")
| @JvmStatic
| fun <T : st.orm.Data> instance(): $metaClassName<T> = INSTANCE as $metaClassName<T>
| }"""
} else {
""
}
}
|}
""".trimMargin(),
)
Expand Down
Loading