diff --git a/storm-core/src/main/java/st/orm/core/repository/impl/WriteSetImpl.java b/storm-core/src/main/java/st/orm/core/repository/impl/WriteSetImpl.java index 0857d4ba4..2904c2f8b 100644 --- a/storm-core/src/main/java/st/orm/core/repository/impl/WriteSetImpl.java +++ b/storm-core/src/main/java/st/orm/core/repository/impl/WriteSetImpl.java @@ -564,6 +564,26 @@ private record RebuildType(@Nonnull RecordType recordType, @Nullable Instantiato Object newInstance(@Nonnull Object[] args) { return instantiator != null ? instantiator.instantiate(args) : recordType.newInstance(args); } + + /** + * Reads the record's component values in declaration order, through the generated deconstructor when the + * metamodel registered one, so rebuilds run as generated code on both sides of the round trip. + */ + @SuppressWarnings("unchecked") + Object[] deconstruct(@Nonnull Object record) { + if (instantiator != null) { + Object[] args = ((Instantiator) instantiator).deconstruct(record); + if (args != null) { + return args; + } + } + List fields = recordType.fields(); + Object[] args = new Object[fields.size()]; + for (int i = 0; i < fields.size(); i++) { + args[i] = REFLECTION.invoke(fields.get(i), record); + } + return args; + } } private static final ClassValue REBUILD_TYPES = new ClassValue<>() { @@ -584,11 +604,7 @@ protected RebuildType computeValue(Class type) { */ private Object withComponent(@Nonnull Object record, @Nonnull int[] path, int depth, @Nullable Object newValue) { RebuildType rebuildType = REBUILD_TYPES.get(record.getClass()); - List fields = rebuildType.recordType().fields(); - Object[] args = new Object[fields.size()]; - for (int i = 0; i < fields.size(); i++) { - args[i] = REFLECTION.invoke(fields.get(i), record); - } + Object[] args = rebuildType.deconstruct(record); int index = path[depth]; args[index] = depth == path.length - 1 ? newValue diff --git a/storm-foundation/src/main/java/st/orm/mapping/Instantiator.java b/storm-foundation/src/main/java/st/orm/mapping/Instantiator.java index fae151b97..7a83047e1 100644 --- a/storm-foundation/src/main/java/st/orm/mapping/Instantiator.java +++ b/storm-foundation/src/main/java/st/orm/mapping/Instantiator.java @@ -16,6 +16,7 @@ package st.orm.mapping; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; /** * Constructs record instances without reflection. @@ -51,4 +52,24 @@ public interface Instantiator { * @return the constructed instance. */ T instantiate(@Nonnull Object[] args); + + /** + * Deconstructs the given instance into its canonical constructor arguments, in declaration order. + * + *

Generated instantiators override this to read the components directly, completing the reflection-free + * round trip for record rebuilds: component reads run as generated code, matching {@link #instantiate} on the + * construction side. The returned array is freshly allocated; callers may modify it and pass it to + * {@link #instantiate} to build an adjusted copy of the instance.

+ * + *

The default returns {@code null}, signalling that no generated deconstructor is available; callers fall + * back to reflective component access.

+ * + * @param instance the instance to deconstruct. + * @return the component values in declaration order, or {@code null} when not supported. + * @since 1.13 + */ + @Nullable + default Object[] deconstruct(@Nonnull T instance) { + return null; + } } diff --git a/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt b/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt index c0c30f84d..779253db3 100644 --- a/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt +++ b/storm-metamodel-ksp/src/main/kotlin/st/orm/metamodel/MetamodelProcessor.kt @@ -991,6 +991,8 @@ class MetamodelProcessor( val arguments = primaryConstructor.parameters.mapIndexed { index, parameter -> " args[$index] as ${getKotlinValueTypeName(parameter.type, packageName)}" }.joinToString(",\n") + val componentNames = primaryConstructor.parameters.map { it.name?.asString() ?: return } + val components = componentNames.joinToString(",\n") { " instance.`$it`" } val containingFile = classDeclaration.containingFile val deps = if (containingFile != null) Dependencies(true, containingFile) else Dependencies(false) val file = codeGenerator.createNewFile( @@ -1016,6 +1018,10 @@ class MetamodelProcessor( | override fun instantiate(args: Array): $className = $className( |$arguments | ) + | + | override fun deconstruct(instance: $className): Array = arrayOf( + |$components + | ) |} """.trimMargin(), ) diff --git a/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java b/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java index f9456e43f..a6e73fed9 100644 --- a/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java +++ b/storm-metamodel-processor/src/main/java/st/orm/metamodel/MetamodelProcessor.java @@ -906,10 +906,13 @@ private void generateInstantiator(@Nonnull Element recordElement) { String instantiatorName = recordName + "Instantiator"; var parameters = constructor.getParameters(); StringBuilder arguments = new StringBuilder(); + StringBuilder components = new StringBuilder(); for (int i = 0; i < parameters.size(); i++) { String castType = getBoxedTypeName(parameters.get(i).asType().toString().replaceAll("@\\S+\\s+", "")); arguments.append(arguments.isEmpty() ? "" : ",\n") .append(" (").append(castType).append(") args[").append(i).append("]"); + components.append(components.isEmpty() ? "" : ",\n") + .append(" instance.").append(parameters.get(i).getSimpleName()).append("()"); } try { JavaFileObject fileObject = processingEnv.getFiler() @@ -919,7 +922,7 @@ private void generateInstantiator(@Nonnull Element recordElement) { %simport javax.annotation.processing.Generated; /** - * Instantiator for %s; constructs instances without reflection. + * Instantiator for %s; constructs and deconstructs instances without reflection. */ @Generated("%s") public final class %s implements st.orm.mapping.Instantiator<%s> { @@ -936,6 +939,13 @@ public final class %s implements st.orm.mapping.Instantiator<%s> { %s ); } + + @Override + public Object[] deconstruct(%s instance) { + return new Object[] { + %s + }; + } }""", (packageName.isEmpty() ? "" : "package " + packageName + ";\n\n"), recordName, @@ -946,7 +956,9 @@ public final class %s implements st.orm.mapping.Instantiator<%s> { recordName, recordName, recordName, - arguments + arguments, + recordName, + components )); } generatedInstantiators.add((packageName.isEmpty() ? "" : packageName + ".") + instantiatorName);