From 434d9035c54fec75206bbd1a1c9ecc460fa071f1 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 18 Sep 2026 18:11:27 +1200 Subject: [PATCH 1/2] Dto mapper generation support for @DbArray Currently it is not detected DbArray and instead thinking its a ToMany which is invalid. --- .../querybean/generator/DtoMapperWriter.java | 3 +- .../querybean/generator/DtoMappingReader.java | 20 +++- .../querybean/generator/DtoPropertyMeta.java | 21 +++- .../generator/ProcessingContext.java | 4 +- .../generator/DtoMapperDbArrayTest.java | 101 ++++++++++++++++++ 5 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java index b3e73ea2d8..acd3d78f66 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java @@ -237,6 +237,7 @@ private List fetchGroupChainCalls(Set excludedFieldNames) { && !property.hasComputedSegment()) { nestedAssocPaths.add(property.sourcePropertyPath().get(0)); } else if (property.kind() == DtoPropertyMeta.Kind.SCALAR && property.isListTarget() + && !property.isScalarCollection() && !property.hasComputedSegment() && property.sourcePropertyPath().size() == 1) { // a single-segment SCALAR property whose DTO field is a List with no registered nested // DTO mapping of its own (e.g. @DtoConvert reducing a ToMany association) - still fully @@ -277,7 +278,7 @@ private List fetchGroupChainCalls(Set excludedFieldNames) { } List path = property.sourcePropertyPath(); if (path.size() == 1) { - if (property.isListTarget()) { + if (property.isListTarget() && !property.isScalarCollection()) { // a single-segment path whose DTO field type is a List, but with no registered // nested DTO mapping of its own (e.g. a @DtoConvert-backed property reducing a // ToMany association to a simpler element type) - the source side is still a real diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java index f6b0a5d200..58a78e6a2b 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java @@ -819,11 +819,12 @@ private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta) // straight into a NullPointerException. Default to the primitive's zero-equivalent value, // or fail fast with a clear message instead when @DtoPath(failOnNull = true). boolean isListTarget = listElementType(field.asType()) != null; + boolean scalarCollection = isListTarget && isDbArrayProperty(lastOwnerType, properties.get(properties.size() - 1)); DtoConverterMeta pathConverter = isListTarget ? converter : autoTypeConverter(converter, lastOwnerType != null ? getterReturnTypeMirror(lastOwnerType, lastGetter) : null, field.asType()); return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, pathConverter, field.asType().getKind().isPrimitive(), pathPrism.failOnNull(), computedFrom >= 0, requiredFetchPaths, - isListTarget, false); + isListTarget, scalarCollection, false); } TypeMirror fieldType = field.asType(); TypeMirror listElementType = listElementType(fieldType); @@ -841,10 +842,11 @@ private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta) } } String getter = getterName(meta.source(), name); + boolean scalarCollection = listElementType != null && isDbArrayProperty(meta.source(), name); DtoConverterMeta scalarConverter = listElementType != null ? converter : autoTypeConverter(converter, getterReturnTypeMirror(meta.source(), getter), fieldType); return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getter), List.of(name), null, scalarConverter, - fieldType.getKind().isPrimitive(), listElementType != null); + fieldType.getKind().isPrimitive(), false, false, List.of(), listElementType != null, scalarCollection, false); } /** @@ -1062,6 +1064,20 @@ private boolean hasField(TypeElement type, String propertyName) { return false; } + private boolean isDbArrayProperty(TypeElement type, String propertyName) { + if (type == null) { + return false; + } + for (TypeElement current = type; current != null; current = superclassOf(current)) { + for (VariableElement field : ElementFilter.fieldsIn(current.getEnclosedElements())) { + if (field.getSimpleName().contentEquals(propertyName)) { + return ctx.isDbArrayField(field); + } + } + } + return false; + } + /** * Whether a no-arg method named {@code methodName} exists on {@code type} (searching * {@code type} and its superclass chain), optionally constrained to a specific return diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java index d3ca8d21cd..91a784de9c 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java @@ -30,6 +30,7 @@ enum Kind { private final boolean computedSegment; private final List requiredFetchPaths; private final boolean listTarget; + private final boolean scalarCollection; private final boolean ignored; DtoPropertyMeta(String dtoFieldName, Kind kind, List sourceGetterPath, List sourcePropertyPath, DtoBeanMeta nested) { @@ -44,7 +45,7 @@ enum Kind { * before ever consulting them. */ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) { - return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, true); + return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, false, true); } /** @@ -90,6 +91,14 @@ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) { DtoPropertyMeta(String dtoFieldName, Kind kind, List sourceGetterPath, List sourcePropertyPath, DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull, boolean computedSegment, List requiredFetchPaths, boolean listTarget, boolean ignored) { + this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, failOnNull, + computedSegment, requiredFetchPaths, listTarget, false, ignored); + } + + DtoPropertyMeta(String dtoFieldName, Kind kind, List sourceGetterPath, List sourcePropertyPath, + DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull, + boolean computedSegment, List requiredFetchPaths, boolean listTarget, + boolean scalarCollection, boolean ignored) { this.dtoFieldName = dtoFieldName; this.kind = kind; this.sourceGetterPath = sourceGetterPath; @@ -101,6 +110,7 @@ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) { this.computedSegment = computedSegment; this.requiredFetchPaths = requiredFetchPaths; this.listTarget = listTarget; + this.scalarCollection = scalarCollection; this.ignored = ignored; } @@ -177,6 +187,14 @@ boolean isListTarget() { return listTarget || kind == Kind.NESTED_MANY; } + /** + * Return true when the source property is a scalar collection such as an Ebean {@code @DbArray}, + * rather than a to-many association. + */ + boolean isScalarCollection() { + return scalarCollection; + } + /** * {@code true} when this property is marked {@code @DtoIgnore} - permanently excluded from * every mapping (base and every named variant alike), always given its empty default rather @@ -245,4 +263,3 @@ private void appendGuardedChain(StringBuilder sb, String prefix, int index) { sb.append(')'); } } - diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java index 6f25d3c2a0..f7800b7b8e 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java @@ -275,7 +275,7 @@ private static boolean dbJsonField(Element field) { /** * Return true if it is a DbArray field. */ - private static boolean dbArrayField(Element field) { + boolean isDbArrayField(Element field) { return hasAnnotations(field, DBARRAY); } @@ -420,7 +420,7 @@ PropertyType getPropertyType(VariableElement field) { if (dbJsonField(field)) { return propertyTypeMap.getDbJsonType(); } - if (dbArrayField(field)) { + if (isDbArrayField(field)) { // get generic parameter type DeclaredType declaredType = (DeclaredType) field.asType(); TypeMirror arrayElementType = declaredType.getTypeArguments().get(0); diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java new file mode 100644 index 0000000000..7ab5508074 --- /dev/null +++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java @@ -0,0 +1,101 @@ +package io.ebean.querybean.generator; + +import org.junit.jupiter.api.Test; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DtoMapperDbArrayTest { + + @Test + void dbArrayList_isSelectedAsScalarColumn() throws IOException { + Path sourceDir = Files.createTempDirectory("dto-db-array-src"); + Path outDir = Files.createTempDirectory("dto-db-array-out"); + Path genSourceDir = Files.createTempDirectory("dto-db-array-gensrc"); + + writeSource(sourceDir, "org.tests.dbarray.ProcessLog", + "package org.tests.dbarray;\n" + + "import io.ebean.annotation.DbArray;\n" + + "public class ProcessLog {\n" + + " @DbArray private java.util.List sourceIds;\n" + + " public java.util.List sourceIds() { return sourceIds; }\n" + + "}\n"); + writeSource(sourceDir, "org.tests.dbarray.ProcessLogDto", + "package org.tests.dbarray;\n" + + "public class ProcessLogDto {\n" + + " private final java.util.List sourceIds;\n" + + " public ProcessLogDto(java.util.List sourceIds) { this.sourceIds = sourceIds; }\n" + + " public java.util.List getSourceIds() { return sourceIds; }\n" + + "}\n"); + writeSource(sourceDir, "org.tests.dbarray.package-info", + "@io.ebean.annotation.DtoMapping(source = ProcessLog.class, target = ProcessLogDto.class)\n" + + "package org.tests.dbarray;\n"); + writeSource(sourceDir, "io.ebean.typequery.Generated", + "package io.ebean.typequery;\n" + + "public @interface Generated { String value(); }\n"); + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, Locale.getDefault(), null)) { + List sourceFiles; + try (var walk = Files.walk(sourceDir)) { + sourceFiles = walk.filter(path -> path.toString().endsWith(".java")).collect(Collectors.toList()); + } + Iterable compilationUnits = fileManager.getJavaFileObjectsFromPaths(sourceFiles); + List options = List.of( + "-d", outDir.toString(), + "-s", genSourceDir.toString(), + "-classpath", System.getProperty("java.class.path"), + "-processor", Processor.class.getName()); + + JavaCompiler.CompilationTask task = compiler.getTask( + null, fileManager, diagnostics, options, null, compilationUnits); + assertTrue(task.call(), "compilation failed: " + errors(diagnostics)); + + Path mapperFile; + try (var walk = Files.walk(genSourceDir)) { + mapperFile = walk.filter(path -> path.getFileName().toString().equals("ProcessLogDtoMapper.java")) + .findFirst() + .orElseThrow(() -> new AssertionError("generated mapper source not found")); + } + String generated = Files.readString(mapperFile); + assertTrue(generated.contains("select(\"sourceIds\")"), generated); + assertTrue(generated.contains("source.sourceIds()"), generated); + assertTrue(!generated.contains("fetch(\"sourceIds\")"), generated); + } + } + + private List errors(DiagnosticCollector diagnostics) { + return diagnostics.getDiagnostics().stream() + .filter(diagnostic -> diagnostic.getKind() == Diagnostic.Kind.ERROR) + .map(diagnostic -> diagnostic.getMessage(Locale.getDefault())) + .collect(Collectors.toList()); + } + + private void writeSource(Path sourceDir, String fqn, String content) { + try { + Path packageDir = sourceDir.resolve(fqn.substring(0, fqn.lastIndexOf('.')).replace('.', '/')); + Files.createDirectories(packageDir); + String simpleName = fqn.substring(fqn.lastIndexOf('.') + 1); + try (Writer writer = Files.newBufferedWriter(packageDir.resolve(simpleName + ".java"))) { + writer.write(content); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} From 30c203cc5ceb17a2c01ebecd61e42103431815b8 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 18 Sep 2026 18:18:26 +1200 Subject: [PATCH 2/2] Dto mapper generation support for @DbJson collections Currently it is not detected and instead thinking its a ToMany which is invalid. --- .../querybean/generator/DtoMapperWriter.java | 5 +-- .../querybean/generator/DtoMappingReader.java | 33 ++++++++++++++----- .../querybean/generator/DtoPropertyMeta.java | 16 ++++++++- .../generator/ProcessingContext.java | 17 ++++++++-- .../generator/DtoMapperDbArrayTest.java | 27 +++++++++++++-- 5 files changed, 83 insertions(+), 15 deletions(-) diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java index acd3d78f66..2422ed4658 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMapperWriter.java @@ -234,6 +234,7 @@ private List fetchGroupChainCalls(Set excludedFieldNames) { Set nestedAssocPaths = new LinkedHashSet<>(); for (DtoPropertyMeta property : activeProperties) { if ((property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY) + && !property.isUnfetchable() && !property.hasComputedSegment()) { nestedAssocPaths.add(property.sourcePropertyPath().get(0)); } else if (property.kind() == DtoPropertyMeta.Kind.SCALAR && property.isListTarget() @@ -254,7 +255,7 @@ private List fetchGroupChainCalls(Set excludedFieldNames) { switch (property.kind()) { case NESTED_ONE: case NESTED_MANY: - if (property.hasComputedSegment()) { + if (property.hasComputedSegment() || property.isUnfetchable()) { // a single-hop @DtoPath rename traversing a computed/derived getter (no backing // field) that happens to target a nested DTO type - just as unfetchable via // fetch(path, mapper.fetchGroup()) as the analogous SCALAR case, since "path" here @@ -268,7 +269,7 @@ private List fetchGroupChainCalls(Set excludedFieldNames) { property.sourcePropertyPath().get(0), mapperFieldName(property))); break; case SCALAR: - if (property.hasComputedSegment()) { + if (property.hasComputedSegment() || property.isUnfetchable()) { // the path traverses a computed/derived getter (no backing field) - its own segments // past that point aren't real Ebean fetch paths, so don't add them to pathSelect/ // rootSelect at all; @DtoPath#requires() (plus the real prefix, if any) already names diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java index 58a78e6a2b..0c0bcd87c1 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoMappingReader.java @@ -819,7 +819,7 @@ private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta) // straight into a NullPointerException. Default to the primitive's zero-equivalent value, // or fail fast with a clear message instead when @DtoPath(failOnNull = true). boolean isListTarget = listElementType(field.asType()) != null; - boolean scalarCollection = isListTarget && isDbArrayProperty(lastOwnerType, properties.get(properties.size() - 1)); + boolean scalarCollection = isListTarget && isScalarCollectionProperty(lastOwnerType, properties.get(properties.size() - 1)); DtoConverterMeta pathConverter = isListTarget ? converter : autoTypeConverter(converter, lastOwnerType != null ? getterReturnTypeMirror(lastOwnerType, lastGetter) : null, field.asType()); return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, pathConverter, @@ -828,25 +828,28 @@ private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta) } TypeMirror fieldType = field.asType(); TypeMirror listElementType = listElementType(fieldType); + boolean unfetchable = isUnfetchableProperty(meta.source(), name); if (listElementType != null) { DtoBeanMeta nested = lookupByTarget(listElementType); if (nested != null) { rejectConverterOnNested(field, converter, name, meta); - return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, List.of(getterName(meta.source(), name)), List.of(name), nested); + return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, List.of(getterName(meta.source(), name)), List.of(name), nested, + unfetchable, List.of()); } } else { DtoBeanMeta nested = lookupByTarget(fieldType); if (nested != null) { rejectConverterOnNested(field, converter, name, meta); - return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, List.of(getterName(meta.source(), name)), List.of(name), nested); + return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, List.of(getterName(meta.source(), name)), List.of(name), nested, + unfetchable, List.of()); } } String getter = getterName(meta.source(), name); - boolean scalarCollection = listElementType != null && isDbArrayProperty(meta.source(), name); + boolean scalarCollection = listElementType != null && isScalarCollectionProperty(meta.source(), name); DtoConverterMeta scalarConverter = listElementType != null ? converter : autoTypeConverter(converter, getterReturnTypeMirror(meta.source(), getter), fieldType); return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getter), List.of(name), null, scalarConverter, - fieldType.getKind().isPrimitive(), false, false, List.of(), listElementType != null, scalarCollection, false); + fieldType.getKind().isPrimitive(), false, false, List.of(), listElementType != null, scalarCollection, unfetchable, false); } /** @@ -1057,21 +1060,35 @@ private boolean hasField(TypeElement type, String propertyName) { for (TypeElement current = type; current != null; current = superclassOf(current)) { for (VariableElement f : ElementFilter.fieldsIn(current.getEnclosedElements())) { if (f.getSimpleName().contentEquals(propertyName)) { - return true; + return !ctx.isTransientField(f); + } + } + } + return false; + } + + private boolean isScalarCollectionProperty(TypeElement type, String propertyName) { + if (type == null) { + return false; + } + for (TypeElement current = type; current != null; current = superclassOf(current)) { + for (VariableElement field : ElementFilter.fieldsIn(current.getEnclosedElements())) { + if (field.getSimpleName().contentEquals(propertyName)) { + return ctx.isScalarCollectionField(field); } } } return false; } - private boolean isDbArrayProperty(TypeElement type, String propertyName) { + private boolean isUnfetchableProperty(TypeElement type, String propertyName) { if (type == null) { return false; } for (TypeElement current = type; current != null; current = superclassOf(current)) { for (VariableElement field : ElementFilter.fieldsIn(current.getEnclosedElements())) { if (field.getSimpleName().contentEquals(propertyName)) { - return ctx.isDbArrayField(field); + return ctx.isTransientField(field); } } } diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java index 91a784de9c..c04f6bb09d 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/DtoPropertyMeta.java @@ -31,6 +31,7 @@ enum Kind { private final List requiredFetchPaths; private final boolean listTarget; private final boolean scalarCollection; + private final boolean unfetchable; private final boolean ignored; DtoPropertyMeta(String dtoFieldName, Kind kind, List sourceGetterPath, List sourcePropertyPath, DtoBeanMeta nested) { @@ -45,7 +46,7 @@ enum Kind { * before ever consulting them. */ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) { - return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, false, true); + return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, false, false, true); } /** @@ -99,6 +100,14 @@ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) { DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull, boolean computedSegment, List requiredFetchPaths, boolean listTarget, boolean scalarCollection, boolean ignored) { + this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, failOnNull, + computedSegment, requiredFetchPaths, listTarget, scalarCollection, false, ignored); + } + + DtoPropertyMeta(String dtoFieldName, Kind kind, List sourceGetterPath, List sourcePropertyPath, + DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull, + boolean computedSegment, List requiredFetchPaths, boolean listTarget, + boolean scalarCollection, boolean unfetchable, boolean ignored) { this.dtoFieldName = dtoFieldName; this.kind = kind; this.sourceGetterPath = sourceGetterPath; @@ -111,6 +120,7 @@ static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) { this.requiredFetchPaths = requiredFetchPaths; this.listTarget = listTarget; this.scalarCollection = scalarCollection; + this.unfetchable = unfetchable; this.ignored = ignored; } @@ -195,6 +205,10 @@ boolean isScalarCollection() { return scalarCollection; } + boolean isUnfetchable() { + return unfetchable; + } + /** * {@code true} when this property is marked {@code @DtoIgnore} - permanently excluded from * every mapping (base and every named variant alike), always given its empty default rather diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java index f7800b7b8e..89df7fa404 100644 --- a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java @@ -223,6 +223,15 @@ private boolean isStaticOrTransient(VariableElement field) { ); } + boolean isTransientField(Element field) { + if (field.getKind() != ElementKind.FIELD) { + return false; + } + VariableElement variable = (VariableElement) field; + return variable.getModifiers().contains(Modifier.TRANSIENT) + || hasAnnotations(variable, "jakarta.persistence.Transient"); + } + private static boolean hasAnnotations(Element element, String... annotations) { return getAnnotation(element, annotations) != null; } @@ -268,7 +277,7 @@ String findDbName(TypeElement element) { /** * Return true if it is a DbJson field. */ - private static boolean dbJsonField(Element field) { + boolean isDbJsonField(Element field) { return hasAnnotations(field, DBJSON, DBJSONB); } @@ -279,6 +288,10 @@ boolean isDbArrayField(Element field) { return hasAnnotations(field, DBARRAY); } + boolean isScalarCollectionField(Element field) { + return isDbArrayField(field) || isDbJsonField(field); + } + private static boolean dbToMany(Element field) { return hasAnnotations(field, ONE_TO_MANY, MANY_TO_MANY); } @@ -417,7 +430,7 @@ PropertyType getPropertyType(VariableElement field) { } boolean toMany = dbToMany(field); - if (dbJsonField(field)) { + if (isDbJsonField(field)) { return propertyTypeMap.getDbJsonType(); } if (isDbArrayField(field)) { diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java index 7ab5508074..df753a706b 100644 --- a/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java +++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/DtoMapperDbArrayTest.java @@ -30,16 +30,34 @@ void dbArrayList_isSelectedAsScalarColumn() throws IOException { writeSource(sourceDir, "org.tests.dbarray.ProcessLog", "package org.tests.dbarray;\n" + "import io.ebean.annotation.DbArray;\n" + + "import io.ebean.annotation.DbJson;\n" + + "import io.ebean.annotation.DbJsonB;\n" + + "import jakarta.persistence.Transient;\n" + "public class ProcessLog {\n" + " @DbArray private java.util.List sourceIds;\n" + + " @DbJson private java.util.List jsonIds;\n" + + " @DbJsonB private java.util.List jsonbIds;\n" + + " @Transient private String computed;\n" + " public java.util.List sourceIds() { return sourceIds; }\n" + + " public java.util.List jsonIds() { return jsonIds; }\n" + + " public java.util.List jsonbIds() { return jsonbIds; }\n" + + " public String computed() { return computed; }\n" + "}\n"); writeSource(sourceDir, "org.tests.dbarray.ProcessLogDto", "package org.tests.dbarray;\n" + "public class ProcessLogDto {\n" + " private final java.util.List sourceIds;\n" - + " public ProcessLogDto(java.util.List sourceIds) { this.sourceIds = sourceIds; }\n" + + " private final java.util.List jsonIds;\n" + + " private final java.util.List jsonbIds;\n" + + " private final String computed;\n" + + " public ProcessLogDto(java.util.List sourceIds, java.util.List jsonIds,\n" + + " java.util.List jsonbIds, String computed) {\n" + + " this.sourceIds = sourceIds; this.jsonIds = jsonIds; this.jsonbIds = jsonbIds; this.computed = computed;\n" + + " }\n" + " public java.util.List getSourceIds() { return sourceIds; }\n" + + " public java.util.List getJsonIds() { return jsonIds; }\n" + + " public java.util.List getJsonbIds() { return jsonbIds; }\n" + + " public String getComputed() { return computed; }\n" + "}\n"); writeSource(sourceDir, "org.tests.dbarray.package-info", "@io.ebean.annotation.DtoMapping(source = ProcessLog.class, target = ProcessLogDto.class)\n" @@ -73,9 +91,14 @@ void dbArrayList_isSelectedAsScalarColumn() throws IOException { .orElseThrow(() -> new AssertionError("generated mapper source not found")); } String generated = Files.readString(mapperFile); - assertTrue(generated.contains("select(\"sourceIds\")"), generated); + assertTrue(generated.contains("select(\"sourceIds,jsonIds,jsonbIds\")"), generated); assertTrue(generated.contains("source.sourceIds()"), generated); + assertTrue(generated.contains("source.jsonIds()"), generated); + assertTrue(generated.contains("source.jsonbIds()"), generated); + assertTrue(generated.contains("source.computed()"), generated); assertTrue(!generated.contains("fetch(\"sourceIds\")"), generated); + assertTrue(!generated.contains("fetch(\"jsonIds\")"), generated); + assertTrue(!generated.contains("fetch(\"jsonbIds\")"), generated); } }