Add polymorphic aggregate root support via sealed interfaces#20
Add polymorphic aggregate root support via sealed interfaces#20stijnvanbael merged 22 commits intomainfrom
Conversation
…erfaces Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/027deafc-27fc-4c7d-aa98-03ab24653ec2 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…, runtime TODOs, open questions Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/cf2390fd-1fcc-40e9-974d-d99d32450e5f Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/7c1a77c0-4afe-49e6-984c-721833df9863 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…ction Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/5b2d2712-a47a-42ec-b840-2057d5f41136 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…gates Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/a36587a1-cba6-45d6-b198-f9348dc0ec26 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…gates Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/86c44968-f68f-4250-a182-e18bc02c6008 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…nse method Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/86c44968-f68f-4250-a182-e18bc02c6008 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
… + fix Kafka test setup Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/98e50b41-f827-4ffe-9479-516dd0296e33 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…PersistentEntity classes Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/98e50b41-f827-4ffe-9479-516dd0296e33 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…g Data 4.x compatibility Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/5a6f7dcb-9c4f-46e9-85d4-a930a3a4c46a Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…erter, use direct cast Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/8ac19811-cf87-48a1-8f3b-cfbdbfe3fda4 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for polymorphic aggregate roots modeled as @Aggregate sealed interfaces, spanning annotation-processor generation plus Postgres (Spring Data JDBC) and MongoDB runtime wiring.
Changes:
- Generate repositories, REST layer, DB migrations (single-table +
typediscriminator), and JDBC reading converters for sealed-interface aggregates. - Wire Spring Data JDBC to (a) treat sealed interfaces as persistent entities and (b) route reads through generated polymorphic converters; also populate the
typediscriminator on insert. - Add MongoDB mapping-context support for sealed-interface aggregates and an example MongoDB integration test using the generated HTTP layer.
Reviewed changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/src/main/java/be/appify/prefab/test/kafka/KafkaTestAutoConfiguration.java | Adjust schema-registry bootstrap server port for testcontainers wiring. |
| postgres/src/main/java/be/appify/prefab/postgres/spring/PrefabJdbcConfiguration.java | Auto-register generated polymorphic JDBC reading converters in JdbcCustomConversions. |
| postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/SealedInterfacePersistentEntity.java | New JDBC persistent entity for sealed @Aggregate interfaces (table + id/isNew delegation). |
| postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabPersistentEntity.java | Derive table name from sealed @Aggregate interface for subtypes (single-table mapping). |
| postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabMappingJdbcConverter.java | Delegate reads of sealed-interface aggregates to registered converters. |
| postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabJdbcMappingContext.java | Create SealedInterfacePersistentEntity for sealed @Aggregate interfaces. |
| postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabDataAccessStrategy.java | Populate type discriminator on insert for polymorphic aggregate subtypes. |
| mongodb/src/main/java/be/appify/prefab/mongodb/spring/PrefabMongoConfiguration.java | Add a custom MongoMappingContext bean intended to support sealed-interface aggregates. |
| mongodb/src/main/java/be/appify/prefab/mongodb/spring/data/mongodb/SealedInterfaceMongoPersistentEntity.java | New Mongo persistent entity for sealed @Aggregate interfaces (id/isNew delegation). |
| mongodb/src/main/java/be/appify/prefab/mongodb/spring/data/mongodb/PrefabMongoMappingContext.java | New mapping context allowing sealed @Aggregate interfaces as managed Mongo entities. |
| examples/mongodb/src/main/java/be/appify/prefab/example/mongodb/shape/Shape.java | Example polymorphic sealed-interface aggregate for MongoDB. |
| examples/mongodb/src/test/java/be/appify/prefab/example/mongodb/shape/ShapeIntegrationTest.java | Integration test verifying round-trip + HTTP responses for polymorphic MongoDB aggregate. |
| examples/avro/src/test/resources/application-test.yml | Formatting tweak for schema-registry test property. |
| core/src/main/java/be/appify/prefab/core/spring/data/jdbc/PolymorphicReadingConverter.java | Marker interface for generated polymorphic JDBC reading converters. |
| backlog/tasks/task-013 - Polymorphism.md | Update task status/notes and document the approach and acceptance criteria. |
| annotation-processor/src/main/java/be/appify/prefab/processor/PrefabProcessor.java | Detect sealed @Aggregate types and route through polymorphic generation pipeline. |
| annotation-processor/src/main/java/be/appify/prefab/processor/PrefabPlugin.java | Extend plugin API to receive polymorphic manifests for combined generation. |
| annotation-processor/src/main/java/be/appify/prefab/processor/PolymorphicAggregateManifest.java | New manifest for sealed-interface aggregates and subtype/common-field discovery. |
| annotation-processor/src/main/java/be/appify/prefab/processor/PolymorphicJdbcConverterWriter.java | Generate JDBC @ReadingConverter for sealed-interface aggregates based on type. |
| annotation-processor/src/main/java/be/appify/prefab/processor/PersistenceWriter.java | Generate repositories for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/HttpWriter.java | Generate polymorphic controller + sealed response type with Jackson type info. |
| annotation-processor/src/main/java/be/appify/prefab/processor/ApplicationWriter.java | Generate application services for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/ControllerUtil.java | Add response-type helper for polymorphic manifests. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/getbyid/GetByIdPlugin.java | Enable GetById generation for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/getbyid/GetByIdControllerWriter.java | Generate GetById controller method for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/getbyid/GetByIdServiceWriter.java | Generate GetById service method for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/getlist/GetListPlugin.java | Enable GetList generation for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/getlist/GetListControllerWriter.java | Generate GetList controller method for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/rest/getlist/GetListServiceWriter.java | Generate GetList service method for polymorphic aggregates. |
| annotation-processor/src/main/java/be/appify/prefab/processor/dbmigration/DbMigrationWriter.java | Generate single-table STI schema for polymorphic aggregates incl. type column. |
| annotation-processor/src/main/java/be/appify/prefab/processor/dbmigration/DbMigrationPlugin.java | Pass polymorphic manifests to migration generation. |
| annotation-processor/src/test/java/be/appify/prefab/processor/persistence/PolymorphicConverterWriterTest.java | New compilation/snapshot tests for polymorphic persistence generation. |
| annotation-processor/src/test/java/be/appify/prefab/processor/rest/PolymorphicRestWriterTest.java | New compilation/snapshot tests for polymorphic REST generation. |
| annotation-processor/src/test/resources/persistence/polymorphic/source/Shape.java | Test input for polymorphic persistence generation. |
| annotation-processor/src/test/resources/persistence/polymorphic/expected/ShapeReadingConverter.java | Snapshot of expected generated polymorphic JDBC reading converter. |
| annotation-processor/src/test/resources/rest/polymorphic/source/Shape.java | Test input for polymorphic REST generation. |
| annotation-processor/src/test/resources/rest/polymorphic/expected/ShapeController.java | Snapshot of expected generated controller. |
| annotation-processor/src/test/resources/rest/polymorphic/expected/ShapeResponse.java | Snapshot of expected generated response type. |
| annotation-processor/src/test/resources/rest/polymorphic/expected/ShapeService.java | Snapshot of expected generated service. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return CodeBlock.of("$T.fromId(($T) row.get($S))", Reference.class, innerType.asTypeName(), columnName); | ||
| } else if (fieldType.isSingleValueType()) { | ||
| var innerType = fieldType.fields().getFirst().type(); | ||
| return CodeBlock.of("row.get($S) != null ? new $T(($T) row.get($S)) : null", | ||
| columnName, fieldType.asTypeName(), innerType.asTypeName()); | ||
| } | ||
| return CodeBlock.of("($T) row.get($S)", fieldType.asBoxed().asTypeName(), columnName); | ||
| } | ||
|
|
There was a problem hiding this comment.
The generated reading converter currently does raw casts for scalar fields (e.g. casting DECIMAL columns to Double). In this codebase, double/float fields are migrated as DECIMAL(19,4) (see DataType.typeOf(...)), so JDBC will typically return a BigDecimal (or at least a Number), causing ClassCastException at runtime. Please convert numeric values via Number (e.g. ((Number)v).doubleValue()/longValue()) and handle nulls, and update the expected ShapeReadingConverter snapshot accordingly.
| return CodeBlock.of("$T.fromId(($T) row.get($S))", Reference.class, innerType.asTypeName(), columnName); | |
| } else if (fieldType.isSingleValueType()) { | |
| var innerType = fieldType.fields().getFirst().type(); | |
| return CodeBlock.of("row.get($S) != null ? new $T(($T) row.get($S)) : null", | |
| columnName, fieldType.asTypeName(), innerType.asTypeName()); | |
| } | |
| return CodeBlock.of("($T) row.get($S)", fieldType.asBoxed().asTypeName(), columnName); | |
| } | |
| return CodeBlock.of("$T.fromId($L)", Reference.class, readValueFromRow(innerType, columnName)); | |
| } else if (fieldType.isSingleValueType()) { | |
| var innerType = fieldType.fields().getFirst().type(); | |
| return CodeBlock.of("row.get($S) != null ? new $T($L) : null", | |
| columnName, fieldType.asTypeName(), readValueFromRow(innerType, columnName)); | |
| } | |
| return readValueFromRow(fieldType, columnName); | |
| } | |
| private CodeBlock readValueFromRow(TypeManifest fieldType, String columnName) { | |
| var boxedType = fieldType.asBoxed().asTypeName(); | |
| if (boxedType.equals(ClassName.get(Double.class))) { | |
| return numericConversion(columnName, "doubleValue"); | |
| } else if (boxedType.equals(ClassName.get(Float.class))) { | |
| return numericConversion(columnName, "floatValue"); | |
| } else if (boxedType.equals(ClassName.get(Long.class))) { | |
| return numericConversion(columnName, "longValue"); | |
| } else if (boxedType.equals(ClassName.get(Integer.class))) { | |
| return numericConversion(columnName, "intValue"); | |
| } else if (boxedType.equals(ClassName.get(Short.class))) { | |
| return numericConversion(columnName, "shortValue"); | |
| } else if (boxedType.equals(ClassName.get(Byte.class))) { | |
| return numericConversion(columnName, "byteValue"); | |
| } | |
| return CodeBlock.of("($T) row.get($S)", boxedType, columnName); | |
| } | |
| private CodeBlock numericConversion(String columnName, String methodName) { | |
| return CodeBlock.of("row.get($S) != null ? (($T) row.get($S)).$L() : null", | |
| columnName, Number.class, columnName, methodName); | |
| } |
| .toList(); | ||
| var polymorphicAggregates = environment.getElementsAnnotatedWith(Aggregate.class) | ||
| .stream() | ||
| .filter(element -> element.getModifiers().contains(Modifier.SEALED)) |
There was a problem hiding this comment.
polymorphicAggregates currently includes any @Aggregate element with the sealed modifier, including sealed classes. The runtime JDBC/Mongo wiring added in this PR explicitly assumes a sealed interface (checks type.isInterface()), so processing sealed classes here would generate repositories/converters that cannot work consistently. Consider filtering to element.getKind().isInterface() (and optionally also excluding abstract sealed classes) to keep the processor aligned with the supported model.
| .filter(element -> element.getModifiers().contains(Modifier.SEALED)) | |
| .filter(element -> element.getKind().isInterface() && element.getModifiers().contains(Modifier.SEALED)) |
| @Bean | ||
| @ConditionalOnMissingBean | ||
| public MongoMappingContext mongoMappingContext() { | ||
| return new PrefabMongoMappingContext(); | ||
| } |
There was a problem hiding this comment.
The new MongoMappingContext bean is created as new PrefabMongoMappingContext() without being configured with the application’s MongoCustomConversions (simple type holder) and other defaults normally applied by Spring Boot’s MongoDB auto-configuration (initial entity set, field naming strategy, auto-index creation, etc.). This can break mapping of Prefab single-value types (e.g. Reference) and potentially entity scanning/index behavior. Consider wiring the Prefab mapping context the same way as the default one (at minimum call setSimpleTypeHolder(mongoCustomConversions.getSimpleTypeHolder()), and ensure the initial entity set is populated) rather than returning a bare instance.
|
Build fails please fix |
…nstructor call Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/bef0ff60-9f21-4470-8112-bba628023554 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…nstructor Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/bef0ff60-9f21-4470-8112-bba628023554 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…MigrationWriterTest conflict Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…DbMigrationWriter Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/c0aff1f8-4e91-4d7b-8e7f-e09d484532f1 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…nnotations Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/0ad0d76f-063e-4d95-adbb-93004fefc631 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…vers Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/974f9ea4-bde0-44e7-b651-db30f7b429b8 Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
Original prompt