From d977e749ff6e1c126a9f2d5881b3026adc6fc942 Mon Sep 17 00:00:00 2001 From: dferreira Date: Fri, 10 Oct 2025 15:54:43 +0100 Subject: [PATCH 1/5] creates support for generics in mapped superclasses --- .../generator/ProcessingContext.java | 199 ++++++++++++++++-- 1 file changed, 186 insertions(+), 13 deletions(-) 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 112f6737f1..c5612ed4bd 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 @@ -11,10 +11,12 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -28,6 +30,7 @@ import java.io.Reader; import java.nio.file.NoSuchFileException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -88,6 +91,11 @@ class ProcessingContext implements Constants { */ private String factoryPackage; + /** + * Cache of resolved generic type parameters for mapped superclasses. + */ + private final Map> genericTypeCache = new HashMap<>(); + ProcessingContext(ProcessingEnvironment processingEnv) { this.processingEnv = processingEnv; this.typeUtils = processingEnv.getTypeUtils(); @@ -122,29 +130,70 @@ TypeElement componentAnnotation() { */ List allFields(Element element) { List list = new ArrayList<>(); - gatherProperties(list, element); + gatherProperties(list, element, null); return list; } /** * Recursively gather all the fields (properties) for the given bean element. */ - private void gatherProperties(List fields, Element element) { + private void gatherProperties(List fields, Element element, Map typeParameterMap) { TypeElement typeElement = (TypeElement) element; TypeMirror superclass = typeElement.getSuperclass(); Element mappedSuper = typeUtils.asElement(superclass); if (isMappedSuperOrInheritance(mappedSuper)) { - gatherProperties(fields, mappedSuper); + // Resolve generic type parameters for the superclass + Map superTypeParameterMap = resolveGenericTypes(superclass, typeParameterMap); + gatherProperties(fields, mappedSuper, superTypeParameterMap); } List allFields = ElementFilter.fieldsIn(element.getEnclosedElements()); for (VariableElement field : allFields) { if (!ignoreField(field)) { - fields.add(field); + // Create a wrapper that holds both the field and its resolved type context + if (typeParameterMap != null && !typeParameterMap.isEmpty()) { + fields.add(new ResolvedVariableElement(field, typeParameterMap)); + } else { + fields.add(field); + } } } } + /** + * Wrapper class to hold a VariableElement along with its type parameter resolution context. + * This allows us to pass resolved generic type information along with field elements. + */ + private static class ResolvedVariableElement implements VariableElement { + private final VariableElement delegate; + private final Map typeParameterMap; + + ResolvedVariableElement(VariableElement delegate, Map typeParameterMap) { + this.delegate = delegate; + this.typeParameterMap = new HashMap<>(typeParameterMap); + } + + public Map getTypeParameterMap() { + return typeParameterMap; + } + + // Delegate all VariableElement methods to the original element + @Override public TypeMirror asType() { return delegate.asType(); } + @Override public ElementKind getKind() { return delegate.getKind(); } + @Override public Set getModifiers() { return delegate.getModifiers(); } + @Override public javax.lang.model.element.Name getSimpleName() { return delegate.getSimpleName(); } + @Override public Element getEnclosingElement() { return delegate.getEnclosingElement(); } + @Override public List getEnclosedElements() { return delegate.getEnclosedElements(); } + @Override public List getAnnotationMirrors() { return delegate.getAnnotationMirrors(); } + @Override public A getAnnotation(Class annotationType) { return delegate.getAnnotation(annotationType); } + @Override public A[] getAnnotationsByType(Class annotationType) { return delegate.getAnnotationsByType(annotationType); } + @Override public Object getConstantValue() { return delegate.getConstantValue(); } + @Override public R accept(javax.lang.model.element.ElementVisitor v, P p) { return delegate.accept(v, p); } + @Override public String toString() { return delegate.toString(); } + @Override public boolean equals(Object obj) { return delegate.equals(obj); } + @Override public int hashCode() { return delegate.hashCode(); } + } + /** * Not interested in static, transient or Ebean internal fields. */ @@ -247,7 +296,118 @@ private String trimAnnotations(String type) { return trimAnnotations(remainder); } + /** + * Resolve generic type parameters for a superclass in the context of a subclass. + * This method maps generic type parameters from the superclass to their actual types + * as specified in the subclass declaration. + * + * @param superclassType The superclass type mirror (may be parameterized) + * @param parentTypeParameterMap Existing type parameter mappings from parent context + * @return A map of type parameter names to their resolved TypeMirror instances + */ + private Map resolveGenericTypes(TypeMirror superclassType, Map parentTypeParameterMap) { + Map typeParameterMap = new HashMap<>(); + + // If we have parent type parameter mappings, inherit them + if (parentTypeParameterMap != null) { + typeParameterMap.putAll(parentTypeParameterMap); + } + + if (superclassType.getKind() != TypeKind.DECLARED) { + return typeParameterMap; + } + + DeclaredType declaredSuperclass = (DeclaredType) superclassType; + TypeElement superclassElement = (TypeElement) declaredSuperclass.asElement(); + + // Get the type parameters from the superclass definition + List typeParameters = superclassElement.getTypeParameters(); + + // Get the actual type arguments used in this specific inheritance + List typeArguments = declaredSuperclass.getTypeArguments(); + + // Map each type parameter to its actual type + for (int i = 0; i < typeParameters.size() && i < typeArguments.size(); i++) { + String parameterName = typeParameters.get(i).getSimpleName().toString(); + TypeMirror actualType = typeArguments.get(i); + + // If the actual type is itself a type variable, try to resolve it from parent context + if (actualType.getKind() == TypeKind.TYPEVAR && parentTypeParameterMap != null) { + TypeVariable typeVar = (TypeVariable) actualType; + String varName = typeVar.asElement().getSimpleName().toString(); + TypeMirror resolvedType = parentTypeParameterMap.get(varName); + if (resolvedType != null) { + actualType = resolvedType; + } + } + + typeParameterMap.put(parameterName, actualType); + } + + // Cache the resolved types for performance + String cacheKey = superclassElement.getQualifiedName().toString(); + genericTypeCache.put(cacheKey, new HashMap<>(typeParameterMap)); + + return typeParameterMap; + } + + /** + * Resolve a field's type in the context of generic type parameters. + * If the field type is a type variable, resolve it to the actual type. + * + * @param fieldType The original field type + * @param typeParameterMap The resolved type parameter mappings + * @return The resolved type mirror, or the original type if no resolution needed + */ + private TypeMirror resolveFieldType(TypeMirror fieldType, Map typeParameterMap) { + if (typeParameterMap == null || typeParameterMap.isEmpty()) { + return fieldType; + } + + if (fieldType.getKind() == TypeKind.TYPEVAR) { + TypeVariable typeVar = (TypeVariable) fieldType; + String parameterName = typeVar.asElement().getSimpleName().toString(); + TypeMirror resolvedType = typeParameterMap.get(parameterName); + if (resolvedType != null) { + return resolvedType; + } + } + + // Handle parameterized types (e.g., List where T needs resolution) + if (fieldType.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) fieldType; + List typeArguments = declaredType.getTypeArguments(); + + if (!typeArguments.isEmpty()) { + List resolvedArguments = new ArrayList<>(); + boolean hasChanges = false; + + for (TypeMirror typeArg : typeArguments) { + TypeMirror resolvedArg = resolveFieldType(typeArg, typeParameterMap); + resolvedArguments.add(resolvedArg); + if (resolvedArg != typeArg) { + hasChanges = true; + } + } + + if (hasChanges) { + // Create a new DeclaredType with resolved type arguments + TypeElement typeElement = (TypeElement) declaredType.asElement(); + return typeUtils.getDeclaredType(typeElement, resolvedArguments.toArray(new TypeMirror[0])); + } + } + } + + return fieldType; + } + PropertyType getPropertyType(VariableElement field) { + // Check if this is a resolved field from a generic mapped superclass + Map typeParameterMap = null; + if (field instanceof ResolvedVariableElement) { + typeParameterMap = ((ResolvedVariableElement) field).getTypeParameterMap(); + } + boolean toMany = dbToMany(field); if (dbJsonField(field)) { return propertyTypeMap.getDbJsonType(); @@ -255,10 +415,17 @@ PropertyType getPropertyType(VariableElement field) { if (dbArrayField(field)) { // get generic parameter type DeclaredType declaredType = (DeclaredType) field.asType(); - String fullType = typeDef(declaredType.getTypeArguments().get(0)); + TypeMirror arrayElementType = declaredType.getTypeArguments().get(0); + // Resolve the array element type if it's generic + TypeMirror resolvedElementType = resolveFieldType(arrayElementType, typeParameterMap); + String fullType = typeDef(resolvedElementType); return new PropertyTypeArray(fullType, Split.shortName(fullType)); } - final TypeMirror typeMirror = field.asType(); + + // Get the field type, potentially resolved if it's generic + final TypeMirror originalTypeMirror = field.asType(); + final TypeMirror typeMirror = resolveFieldType(originalTypeMirror, typeParameterMap); + TypeMirror currentType = typeMirror; while (currentType != null) { PropertyType type = propertyTypeMap.getType(typeDef(currentType)); @@ -278,7 +445,7 @@ PropertyType getPropertyType(VariableElement field) { // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=544288 fieldType = elementUtils.getTypeElement(fieldType.toString()); - if (fieldType.getKind() == ElementKind.ENUM) { + if (fieldType != null && fieldType.getKind() == ElementKind.ENUM) { String fullType = typeDef(typeMirror); return new PropertyTypeEnum(fullType, Split.shortName(fullType)); } @@ -301,7 +468,7 @@ PropertyType getPropertyType(VariableElement field) { final PropertyType result; if (typeMirror.getKind() == TypeKind.DECLARED) { - result = createManyTypeAssoc(field, (DeclaredType) typeMirror); + result = createManyTypeAssoc(field, (DeclaredType) typeMirror, typeParameterMap); } else { result = null; } @@ -333,20 +500,26 @@ private boolean typeInstanceOf(final TypeMirror typeMirror, final CharSequence d .anyMatch(t -> typeInstanceOf(t, desiredInterface)); } - private PropertyType createManyTypeAssoc(VariableElement field, DeclaredType declaredType) { + private PropertyType createManyTypeAssoc(VariableElement field, DeclaredType declaredType, Map typeParameterMap) { boolean toMany = dbToMany(field); List typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() == 1) { - Element argElement = typeUtils.asElement(typeArguments.get(0)); + TypeMirror argType = typeArguments.get(0); + // Resolve the type argument if it's generic + TypeMirror resolvedArgType = resolveFieldType(argType, typeParameterMap); + Element argElement = typeUtils.asElement(resolvedArgType); if (isEntityOrEmbedded(argElement)) { boolean embeddable = isEmbeddable(argElement); - return createPropertyTypeAssoc(embeddable, toMany, typeDef(argElement.asType())); + return createPropertyTypeAssoc(embeddable, toMany, typeDef(resolvedArgType)); } } else if (typeArguments.size() == 2) { - Element argElement = typeUtils.asElement(typeArguments.get(1)); + TypeMirror argType = typeArguments.get(1); + // Resolve the type argument if it's generic + TypeMirror resolvedArgType = resolveFieldType(argType, typeParameterMap); + Element argElement = typeUtils.asElement(resolvedArgType); if (isEntityOrEmbedded(argElement)) { boolean embeddable = isEmbeddable(argElement); - return createPropertyTypeAssoc(embeddable, toMany, typeDef(argElement.asType())); + return createPropertyTypeAssoc(embeddable, toMany, typeDef(resolvedArgType)); } } return null; From e8e7d4b9b96b9e97e313898c4b7f71aaaed6cadf Mon Sep 17 00:00:00 2001 From: dferreira Date: Wed, 15 Oct 2025 15:04:15 +0100 Subject: [PATCH 2/5] Changes DeployCreateProperties to allow processing of inheritance hierarchies with generics --- .../deploy/parse/DeployCreateProperties.java | 91 +++++++++++++++---- 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index 4e887bbbc8..58969bb8c7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -11,6 +11,8 @@ import jakarta.persistence.*; import java.lang.reflect.*; +import java.util.HashMap; +import java.util.Map; import static java.lang.System.Logger.Level.*; @@ -35,7 +37,7 @@ public DeployCreateProperties(TypeManager typeManager) { * Create the appropriate properties for a bean. */ public void createProperties(DeployBeanDescriptor desc) { - createProperties(desc, desc.getBeanType(), 0); + createProperties(desc, desc.getBeanType(), 0, new HashMap<>()); desc.sortProperties(); } @@ -56,15 +58,20 @@ private boolean ignoreFieldByName(String fieldName) { private boolean ignoreField(Field field) { return Modifier.isStatic(field.getModifiers()) - || Modifier.isTransient(field.getModifiers()) - || ignoreFieldByName(field.getName()); + || Modifier.isTransient(field.getModifiers()) + || ignoreFieldByName(field.getName()); } /** - * properties the bean properties from Class. Some of these properties may not map to database + * properties the bean properties from Class. Some of these properties may not + * map to database * columns. */ - private void createProperties(DeployBeanDescriptor desc, Class beanType, int level) { + private void createProperties( + DeployBeanDescriptor desc, + Class beanType, + int level, + Map, Class> genericTypeMap) { if (beanType.equals(Model.class)) { // ignore all fields on model (_$dbName) return; @@ -74,7 +81,7 @@ private void createProperties(DeployBeanDescriptor desc, Class beanType, i for (int i = 0; i < fields.length; i++) { Field field = fields[i]; if (!ignoreField(field)) { - DeployBeanProperty prop = createProp(desc, field, beanType); + DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap); if (prop != null) { // set a order that gives priority to inherited properties // push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down @@ -95,7 +102,7 @@ private void createProperties(DeployBeanDescriptor desc, Class beanType, i if (!superClass.equals(Object.class)) { // recursively add any properties in the inheritance hierarchy // up to the Object.class level... - createProperties(desc, superClass, level + 1); + createProperties(desc, superClass, level + 1, mapGenerics(beanType)); } } catch (PersistenceException ex) { throw ex; @@ -116,8 +123,13 @@ private DeployBeanProperty createManyType(DeployBeanDescriptor desc, Class return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType); } - private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) { - Class propertyType = field.getType(); + private DeployBeanProperty createProp( + DeployBeanDescriptor desc, + Field field, + Map, Class> genericTypeMap) { + Class propertyType = field.getGenericType() instanceof TypeVariable + ? genericTypeMap.get(field.getGenericType()) + : field.getType(); if (isSpecialScalarType(field)) { return new DeployBeanProperty(desc, propertyType, field.getGenericType()); } @@ -131,7 +143,8 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) // not supporting this field (generic type used) return null; } - CoreLog.internal.log(WARNING, "Could not find parameter type (via reflection) on " + desc.getFullName() + " " + field.getName()); + CoreLog.internal.log(WARNING, + "Could not find parameter type (via reflection) on " + desc.getFullName() + " " + field.getName()); } return createManyType(desc, targetType, manyType); } @@ -147,7 +160,8 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) return new DeployBeanProperty(desc, propertyType, null, null); } if (AnnotationUtil.has(field, Convert.class)) { - throw new IllegalStateException("No AttributeConverter registered for type " + propertyType + " at " + desc.getFullName() + "." + field.getName()); + throw new IllegalStateException("No AttributeConverter registered for type " + propertyType + " at " + + desc.getFullName() + "." + field.getName()); } try { return new DeployBeanPropertyAssocOne<>(desc, propertyType); @@ -162,18 +176,22 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field) */ private boolean isSpecialScalarType(Field field) { return (AnnotationUtil.has(field, DbJson.class)) - || (AnnotationUtil.has(field, DbJsonB.class)) - || (AnnotationUtil.has(field, DbArray.class)) - || (AnnotationUtil.has(field, DbMap.class)) - || (AnnotationUtil.has(field, UnmappedJson.class)); + || (AnnotationUtil.has(field, DbJsonB.class)) + || (AnnotationUtil.has(field, DbArray.class)) + || (AnnotationUtil.has(field, DbMap.class)) + || (AnnotationUtil.has(field, UnmappedJson.class)); } private boolean isTransientField(Field field) { return AnnotationUtil.has(field, Transient.class); } - private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field, Class beanType) { - DeployBeanProperty prop = createProp(desc, field); + private DeployBeanProperty createProp( + DeployBeanDescriptor desc, + Field field, + Class beanType, + Map, Class> genericTypeMap) { + DeployBeanProperty prop = createProp(desc, field, genericTypeMap); if (prop == null) { // transient annotation on unsupported type return null; @@ -186,7 +204,8 @@ private DeployBeanProperty createProp(DeployBeanDescriptor desc, Field field, } /** - * Determine the type of the List,Set or Map. Not been set explicitly so determine this from + * Determine the type of the List,Set or Map. Not been set explicitly so + * determine this from * ParameterizedType. */ private Class determineTargetType(Field field) { @@ -224,4 +243,40 @@ private Class determineTargetType(Field field) { // if targetType is null, then must be set in annotations return null; } + + private Map, Class> mapGenerics(Class clazz) { + Type genericSuperclass = clazz.getGenericSuperclass(); + if (!(genericSuperclass instanceof ParameterizedType)) { + return new HashMap<>(); + } + + ParameterizedType parameterized = (ParameterizedType) genericSuperclass; + TypeVariable[] typeVars = ((Class) parameterized.getRawType()).getTypeParameters(); + Type[] actualTypes = parameterized.getActualTypeArguments(); + + Map, Class> typeMap = new HashMap<>(); + for (int i = 0; i < typeVars.length; i++) { + Type actual = actualTypes[i]; + Class resolvedClass = resolveToClass(actual); + if (resolvedClass != null) { + typeMap.put(typeVars[i], resolvedClass); + } else { + // ignore + } + } + return typeMap; + } + + private static Class resolveToClass(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) type; + Type raw = pType.getRawType(); + if (raw instanceof Class) { + return (Class) raw; + } + } + return null; + } } From e9a77fbeff6eadc34a4be118d87f3a4d0d049072 Mon Sep 17 00:00:00 2001 From: dferreira Date: Mon, 24 Nov 2025 11:53:25 +0000 Subject: [PATCH 3/5] adds tests for ebean-querybean --- .../org/example/domain/GenericBaseModel.java | 69 +++++++++++++++++++ .../domain/ProductWithGenericLong.java | 48 +++++++++++++ .../domain/ProductWithGenericString.java | 48 +++++++++++++ .../querytest/QProductWithGenericTest.java | 49 +++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 ebean-querybean/src/test/java/org/example/domain/GenericBaseModel.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java create mode 100644 ebean-querybean/src/test/java/org/querytest/QProductWithGenericTest.java diff --git a/ebean-querybean/src/test/java/org/example/domain/GenericBaseModel.java b/ebean-querybean/src/test/java/org/example/domain/GenericBaseModel.java new file mode 100644 index 0000000000..e1da39d683 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/GenericBaseModel.java @@ -0,0 +1,69 @@ +package org.example.domain; + +import io.ebean.Model; +import io.ebean.annotation.WhenCreated; +import io.ebean.annotation.WhenModified; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; +import java.sql.Timestamp; + +/** + * Base domain object with Id, version, whenCreated and whenUpdated. + * + *

+ * Extending Model to enable the 'active record' style. + * + *

+ * whenCreated and whenUpdated are generally useful for maintaining external search services (like + * elasticsearch) and audit. + */ +@MappedSuperclass +public abstract class GenericBaseModel extends Model { + + @Id + T id; + + @Version + Long version; + + @WhenCreated + Timestamp whenCreated; + + @WhenModified + Timestamp whenUpdated; + + public T getId() { + return id; + } + + public void setId(T id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Timestamp getWhenCreated() { + return whenCreated; + } + + public void setWhenCreated(Timestamp whenCreated) { + this.whenCreated = whenCreated; + } + + public Timestamp getWhenUpdated() { + return whenUpdated; + } + + public void setWhenUpdated(Timestamp whenUpdated) { + this.whenUpdated = whenUpdated; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java new file mode 100644 index 0000000000..ac3780acbd --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java @@ -0,0 +1,48 @@ +package org.example.domain; + +import org.example.domain.ProductWithGenericLong; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import javax.validation.constraints.Size; + +/** + * Product entity bean. + */ +@Entity +@Table(name = "o_product", schema = "foo") +public class ProductWithGenericLong extends GenericBaseModel { + + @Size(max = 20) + String sku; + + String name; + + /** + * Return sku. + */ + public String getSku() { + return sku; + } + + /** + * Set sku. + */ + public void setSku(String sku) { + this.sku = sku; + } + + /** + * Return name. + */ + public String getName() { + return name; + } + + /** + * Set name. + */ + public void setName(String name) { + this.name = name; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java new file mode 100644 index 0000000000..0a79724b85 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java @@ -0,0 +1,48 @@ +package org.example.domain; + +import org.example.domain.ProductWithGenericLong; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import javax.validation.constraints.Size; + +/** + * Product entity bean. + */ +@Entity +@Table(name = "o_product", schema = "foo") +public class ProductWithGenericString extends GenericBaseModel { + + @Size(max = 20) + String sku; + + String name; + + /** + * Return sku. + */ + public String getSku() { + return sku; + } + + /** + * Set sku. + */ + public void setSku(String sku) { + this.sku = sku; + } + + /** + * Return name. + */ + public String getName() { + return name; + } + + /** + * Set name. + */ + public void setName(String name) { + this.name = name; + } + +} diff --git a/ebean-querybean/src/test/java/org/querytest/QProductWithGenericTest.java b/ebean-querybean/src/test/java/org/querytest/QProductWithGenericTest.java new file mode 100644 index 0000000000..d3f19c292a --- /dev/null +++ b/ebean-querybean/src/test/java/org/querytest/QProductWithGenericTest.java @@ -0,0 +1,49 @@ +package org.querytest; + +import io.ebean.InTuples; + +import org.example.domain.ProductWithGenericLong; +import org.example.domain.ProductWithGenericString; +import org.example.domain.query.QContact; +import org.example.domain.query.QProductWithGenericLong; +import org.example.domain.query.QProductWithGenericString; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QProductWithGenericTest { + + @Test + void findByLongId() { + + var entity = new ProductWithGenericLong(); + entity.setId(42L); + entity.setName("Gadget"); + entity.save(); + + var result = new QProductWithGenericLong() + .id.eq(42L) + .findOne(); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("Gadget"); + } + + @Test + void findByStringId() { + + var entity = new ProductWithGenericString(); + entity.setId("1234"); + entity.setName("Gadget"); + entity.save(); + + var result = new QProductWithGenericString() + .id.eq("1234") + .findOne(); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("Gadget"); + } +} From e73a8e8af85941618251ad1795506a3b18d0e1a7 Mon Sep 17 00:00:00 2001 From: dferreira Date: Mon, 24 Nov 2025 12:04:12 +0000 Subject: [PATCH 4/5] bumps querybean-generator version --- querybean-generator/pom.xml | 2 +- tests/test-java16/.factorypath | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/test-java16/.factorypath diff --git a/querybean-generator/pom.xml b/querybean-generator/pom.xml index 9c1d706700..773b183cc1 100644 --- a/querybean-generator/pom.xml +++ b/querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 16.0.1 + 16.1.0 querybean generator diff --git a/tests/test-java16/.factorypath b/tests/test-java16/.factorypath new file mode 100644 index 0000000000..9f7bbd8f81 --- /dev/null +++ b/tests/test-java16/.factorypath @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From 399355822810694c8d23918b8808de1ddf3132f9 Mon Sep 17 00:00:00 2001 From: dferreira Date: Mon, 24 Nov 2025 12:23:35 +0000 Subject: [PATCH 5/5] fixes tests by changing table names for ProductWithGenericLong and ProductWithGenericString --- .../test/java/org/example/domain/ProductWithGenericLong.java | 2 +- .../test/java/org/example/domain/ProductWithGenericString.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java index ac3780acbd..992816cd66 100644 --- a/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java +++ b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericLong.java @@ -9,7 +9,7 @@ * Product entity bean. */ @Entity -@Table(name = "o_product", schema = "foo") +@Table(name = "long_product", schema = "foo") public class ProductWithGenericLong extends GenericBaseModel { @Size(max = 20) diff --git a/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java index 0a79724b85..293b1791f7 100644 --- a/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java +++ b/ebean-querybean/src/test/java/org/example/domain/ProductWithGenericString.java @@ -9,7 +9,7 @@ * Product entity bean. */ @Entity -@Table(name = "o_product", schema = "foo") +@Table(name = "string_product", schema = "foo") public class ProductWithGenericString extends GenericBaseModel { @Size(max = 20)