diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/EntityManagerBuilder.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/EntityManagerBuilder.java index 4049618ca0c..d09afd061c0 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/EntityManagerBuilder.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/EntityManagerBuilder.java @@ -36,6 +36,7 @@ import com.ibm.websphere.ras.annotation.Trivial; import com.ibm.ws.ffdc.annotation.FFDCIgnore; +import io.openliberty.data.internal.persistence.cdi.DataExtensionProvider; import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Attribute.PersistentAttributeType; @@ -65,13 +66,19 @@ public abstract class EntityManagerBuilder implements Runnable { */ final ConcurrentHashMap, CompletableFuture> entityInfoMap = new ConcurrentHashMap<>(); + /** + * OSGi service component that provides the CDI extension for Data. + */ + final DataExtensionProvider provider; + /** * The class loader for repository classes. */ private final ClassLoader repositoryClassLoader; @Trivial - protected EntityManagerBuilder(ClassLoader repositoryClassLoader) { + protected EntityManagerBuilder(DataExtensionProvider provider, ClassLoader repositoryClassLoader) { + this.provider = provider; this.repositoryClassLoader = repositoryClassLoader; } diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java index 82e27166142..ebe92fc97b9 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/QueryInfo.java @@ -19,6 +19,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.lang.reflect.RecordComponent; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; @@ -398,6 +399,121 @@ else if (hasIdClass && ID.equals(sort.property())) return combined; } + /** + * Generates the SELECT clause of the JPQL. + * + * @return the SELECT clause. + */ + StringBuilder generateSelectClause() { + StringBuilder q = new StringBuilder(200); + String o = entityVar; + String o_ = entityVar_; + + String[] cols, selections = entityInfo.builder.provider.compat.getSelections(method); + if (selections == null || selections.length == 0) { + cols = null; + } else if (type == QueryInfo.Type.FIND_AND_DELETE) { + // TODO NLS message for error path once selections are supported function + throw new UnsupportedOperationException(); + } else { + cols = new String[selections.length]; + for (int i = 0; i < cols.length; i++) { + String name = entityInfo.getAttributeName(selections[i], true); + cols[i] = name == null ? selections[i] : name; + } + } + + Class singleType = getSingleResultType(); + + if (singleType.isPrimitive()) + singleType = QueryInfo.wrapperClassIfPrimitive(singleType); + + q.append("SELECT "); + + if (cols == null || cols.length == 0) { + if (singleType.isAssignableFrom(entityInfo.entityClass) + || entityInfo.inheritance && entityInfo.entityClass.isAssignableFrom(singleType)) { + // Whole entity + q.append(o); + } else { + // Look for single entity attribute with the desired type: + String singleAttributeName = null; + for (Map.Entry> entry : entityInfo.attributeTypes.entrySet()) { + Class collectionElementType = entityInfo.collectionElementTypes.get(entry.getKey()); + Class attributeType = collectionElementType == null ? entry.getValue() : collectionElementType; + if (attributeType.isPrimitive()) + attributeType = QueryInfo.wrapperClassIfPrimitive(attributeType); + if (singleType.isAssignableFrom(attributeType)) { + singleAttributeName = entry.getKey(); + q.append(o_).append(singleAttributeName); + break; + } + } + + if (singleAttributeName == null) { + // Construct new instance from IdClass, embeddable, or entity attributes. + // It would be preferable if the spec included the Select annotation to explicitly identify parameters, but if that doesn't happen + // TODO we could compare attribute types with known constructor to improve on guessing a correct order of parameters + q.append("NEW ").append(singleType.getName()).append('('); + List relAttrNames; + boolean first = true; + if (entityInfo.idClassAttributeAccessors != null && singleType.equals(entityInfo.idType)) + for (String idClassAttributeName : entityInfo.idClassAttributeAccessors.keySet()) { + String name = entityInfo.getAttributeName(idClassAttributeName, true); + q.append(first ? "" : ", ").append(o).append('.').append(name); + first = false; + } + else if ((relAttrNames = entityInfo.relationAttributeNames.get(singleType)) != null) + for (String name : relAttrNames) { + q.append(first ? "" : ", ").append(o).append('.').append(name); + first = false; + } + else if (entityInfo.recordClass == null) + for (String name : entityInfo.attributeTypes.keySet()) { + q.append(first ? "" : ", ").append(o).append('.').append(name); + first = false; + } + else { + for (RecordComponent component : entityInfo.recordClass.getRecordComponents()) { + String name = component.getName(); + q.append(first ? "" : ", ").append(o).append('.').append(name); + first = false; + } + } + q.append(')'); + } + } + } else { // Individual columns are requested by @Select + Class entityType = entityInfo.getType(); + boolean selectAsColumns = singleType.isAssignableFrom(entityType) + || singleType.isInterface() // NEW instance doesn't apply to interfaces + || singleType.isPrimitive() // NEW instance should not be used on primitives + || singleType.getName().startsWith("java") // NEW instance constructor is unlikely for non-user-defined classes + || entityInfo.inheritance && entityType.isAssignableFrom(singleType); + if (!selectAsColumns && cols.length == 1) { + String singleAttributeName = cols[0]; + Class attributeType = entityInfo.collectionElementTypes.get(singleAttributeName); + if (attributeType == null) + attributeType = entityInfo.attributeTypes.get(singleAttributeName); + selectAsColumns = attributeType != null && (Object.class.equals(attributeType) // JPA metamodel does not preserve the type if not an EmbeddableCollection + || singleType.isAssignableFrom(attributeType)); + } + if (selectAsColumns) { + // Specify columns without creating new instance + for (int i = 0; i < cols.length; i++) + q.append(i == 0 ? "" : ", ").append(o).append('.').append(cols[i]); + } else { + // Construct new instance from defined columns + q.append("NEW ").append(singleType.getName()).append('('); + for (int i = 0; i < cols.length; i++) + q.append(i == 0 ? "" : ", ").append(o).append('.').append(cols[i]); + q.append(')'); + } + } + + return q; + } + /** * Locate the entity information for the specified result class. * @@ -417,7 +533,7 @@ EntityInfo getEntityInfo(Class entityType, Map entityType, Map> entityInfos) { + EntityInfo entityInfo; CompletableFuture future = entityInfos.get(entityName); if (future == null) { - // Identify possible case mismatch - for (String name : entityInfos.keySet()) - if (entityName.equalsIgnoreCase(name)) + // When a Java record is used as an entity, the name is [RecordName]Entity + String recordEntityName = entityName + EntityInfo.RECORD_ENTITY_SUFFIX; + future = entityInfos.get(recordEntityName); + if (future == null) { + entityInfo = null; + } else { + entityInfo = future.join(); + if (entityInfo.recordClass == null) + entityInfo = null; + } + + if (entityInfo == null) { + // Identify possible case mismatch + for (String name : entityInfos.keySet()) { + if (recordEntityName.equalsIgnoreCase(name) && entityInfos.get(name).join().recordClass != null) + name = name.substring(0, name.length() - EntityInfo.RECORD_ENTITY_SUFFIX.length()); + if (entityName.equalsIgnoreCase(name)) + throw new MappingException("The " + method.getName() + " method of the " + method.getDeclaringClass().getName() + + " repository specifies query language that requires a " + entityName + + " entity that is not found but is a close match for the " + name + + " entity. Review the query language to ensure the correct entity name is used."); // TODO NLS + } + + future = entityInfos.get(EntityInfo.FAILED); + if (future == null) throw new MappingException("The " + method.getName() + " method of the " + method.getDeclaringClass().getName() + " repository specifies query language that requires a " + entityName + - " entity that is not found but is a close match for the " + name + - " entity. Review the query language to ensure the correct entity name is used."); // TODO NLS - - future = entityInfos.get(EntityInfo.FAILED); - if (future == null) - throw new MappingException("The " + method.getName() + " method of the " + method.getDeclaringClass().getName() + - " repository specifies query language that requires a " + entityName + - " entity that is not found. Check if " + entityName + " is the name of a valid entity." + - " To enable the entity to be found, give the repository a life cycle method that is" + - " annotated with one of " + "(Insert, Save, Update, Delete)" + - " and supply the entity as its parameter or have the repository extend" + - " DataRepository or another built-in repository interface with the entity class as the" + - " first type variable."); // TODO NLS + " entity that is not found. Check if " + entityName + " is the name of a valid entity." + + " To enable the entity to be found, give the repository a life cycle method that is" + + " annotated with one of " + "(Insert, Save, Update, Delete)" + + " and supply the entity as its parameter or have the repository extend" + + " DataRepository or another built-in repository interface with the entity class as the" + + " first type variable."); // TODO NLS + } + } else { + entityInfo = future.join(); } - return future.join(); + return entityInfo; } /** @@ -652,8 +787,8 @@ void initForQuery(String ql, Class multiType, Map multiType, Map 0 && Character.isWhitespace(ql.charAt(startAt))) { + if (startAt + 1 < length && entityInfo.name.length() > 0 && Character.isWhitespace(ql.charAt(startAt))) { for (startAt++; startAt < length && Character.isWhitespace(ql.charAt(startAt)); startAt++); if (startAt + 4 < length && ql.regionMatches(true, startAt, "SET", 0, 3) @@ -694,7 +829,7 @@ else if (entityInfo == null) entityVar = "o"; entityVar_ = "o."; StringBuilder q = new StringBuilder(ql.length() * 3 / 2) // - .append("UPDATE ").append(entityName).append(" o SET"); + .append("UPDATE ").append(entityInfo.name).append(" o SET"); jpql = appendWithIdentifierName(ql, startAt + 3, ql.length(), q).toString(); } } @@ -911,17 +1046,16 @@ else if (order0 >= 0 && orderLen == 0) whereLen += 2 + (addSpace ? 1 : 0); } - StringBuilder q = new StringBuilder(ql.length() + (selectLen >= 0 ? 0 : 50) + (fromLen >= 0 ? 0 : 50) + 2); - q.append("SELECT"); + StringBuilder q; if (selectLen > 0) { + q = new StringBuilder(ql.length() + (selectLen >= 0 ? 0 : 50) + (fromLen >= 0 ? 0 : 50) + 2); + q.append("SELECT"); appendWithIdentifierName(ql, select0, select0 + selectLen, q); - if (fromLen == 0 && whereLen == 0 && orderLen == 0) - q.append(' '); } else { - q.append(' ').append(entityVar).append(' '); + q = generateSelectClause(); } - q.append("FROM"); + q.append(" FROM"); if (fromLen > 0 && !lacksEntityVar) q.append(ql.substring(from0, from0 + fromLen)); else diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java index 66de4e68af6..1fc6505840b 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/RepositoryImpl.java @@ -23,7 +23,6 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import java.lang.reflect.RecordComponent; import java.sql.SQLIntegrityConstraintViolationException; import java.sql.SQLNonTransientConnectionException; import java.sql.SQLRecoverableException; @@ -351,6 +350,8 @@ private QueryInfo completeQueryInfo(Map> e } } + EntityInfo entityInfo = queryInfo.entityInfo; + // If we don't already know from generating the JPQL, find out how many // parameters the JPQL takes and which parameters are named parameters. if (query != null || queryInfo.paramNames != null) { @@ -380,7 +381,6 @@ private QueryInfo completeQueryInfo(Map> e if (paramName != null) { if (queryInfo.paramNames == null) queryInfo.paramNames = new ArrayList<>(); - EntityInfo entityInfo = queryInfo.entityInfo; if (entityInfo.idClassAttributeAccessors != null && paramType.equals(entityInfo.idType)) // TODO is this correct to do when @Query has a named parameter with type of the IdClass? // It seems like the JPQL would not be consistent. @@ -419,7 +419,8 @@ private QueryInfo completeQueryInfo(Map> e queryInfo.sorts = queryInfo.sorts == null ? new ArrayList<>(orderBy.length + 2) : queryInfo.sorts; if (q == null) if (queryInfo.jpql == null) { - q = generateSelectClause(queryInfo); + q = queryInfo.generateSelectClause(); + q.append(" FROM ").append(entityInfo.name).append(' ').append(queryInfo.entityVar); if (countPages) generateCount(queryInfo, null); } else { @@ -1219,6 +1220,7 @@ private void generateCount(QueryInfo queryInfo, String where) { */ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder q, Annotation methodAnno, boolean countPages, boolean hasUpdateParam, ParamInfo[] allParamInfo) { + EntityInfo entityInfo = queryInfo.entityInfo; String o = queryInfo.entityVar; String o_ = queryInfo.entityVar_; @@ -1237,9 +1239,9 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder methodAnno.annotationType().getSimpleName() + " operation."); // TODO NLS // Identify IdClass parameters - if (queryInfo.entityInfo.idClassAttributeAccessors != null) { + if (entityInfo.idClassAttributeAccessors != null) { for (int p = 0; p < numAttributeParams; p++) - if (paramTypes[p].equals(queryInfo.entityInfo.idType)) { + if (paramTypes[p].equals(entityInfo.idType)) { if (allParamInfo[p] == null) allParamInfo[p] = new ParamInfo(); allParamInfo[p].isIdClass = true; @@ -1250,10 +1252,10 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder // Write new JPQL, starting with SELECT or UPDATE if (!hasUpdateParam) { queryInfo.type = QueryInfo.Type.FIND; - q = generateSelectClause(queryInfo); + q = queryInfo.generateSelectClause().append(" FROM ").append(entityInfo.name).append(' ').append(o); } else { queryInfo.type = QueryInfo.Type.UPDATE; - q = new StringBuilder(250).append("UPDATE ").append(queryInfo.entityInfo.name).append(' ').append(o).append(" SET"); + q = new StringBuilder(250).append("UPDATE ").append(entityInfo.name).append(' ').append(o).append(" SET"); boolean first = true; // p is the method parameter number (0-based) @@ -1263,7 +1265,7 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder if (paramInfo != null) if (paramInfo.isIdClass) { if (paramInfo.updateAnno == null) { - qp += queryInfo.entityInfo.idClassAttributeAccessors.size() - 1; + qp += entityInfo.idClassAttributeAccessors.size() - 1; } else if ("=".equals(provider.compat.getUpdateAttributeAndOperation(paramInfo.updateAnno)[1])) { // generateUpdatesForIdClass(queryInfo, update, first, q); throw new UnsupportedOperationException("@Assign IdClass"); // TODO @@ -1293,7 +1295,7 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder " with the -parameters compiler option that preserves the parameter names."); // TODO NLS } - String name = queryInfo.entityInfo.getAttributeName(attribute, true); + String name = entityInfo.getAttributeName(attribute, true); q.append(first ? " " : ", ").append(o_).append(name).append("="); first = false; @@ -1303,7 +1305,7 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder case "=": break; case "+": - if (withFunction = CharSequence.class.isAssignableFrom(queryInfo.entityInfo.attributeTypes.get(name))) + if (withFunction = CharSequence.class.isAssignableFrom(entityInfo.attributeTypes.get(name))) q.append("CONCAT(").append(o_).append(name).append(','); else q.append(o_).append(name).append('+'); @@ -1384,7 +1386,7 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder ? null // Equals : paramInfo.comparisonAnno.annotationType().getSimpleName(); - boolean isCollection = queryInfo.entityInfo.collectionElementTypes.containsKey(name); + boolean isCollection = entityInfo.collectionElementTypes.containsKey(name); if (isCollection) if (paramInfo.comparisonAnno != null && !"Contains".equals(comparisonName) || ignoreCase) throw new MappingException(new UnsupportedOperationException("The parameter annotation " + @@ -1438,7 +1440,7 @@ private StringBuilder generateFromParameters(QueryInfo queryInfo, StringBuilder } } else if (paramInfo.isIdClass) { // adjust query parameter position based on the number of parameters needed for an IdClass - qp += queryInfo.entityInfo.idClassAttributeAccessors.size() - 1; + qp += entityInfo.idClassAttributeAccessors.size() - 1; } } if (queryInfo.hasWhere) @@ -1568,7 +1570,7 @@ private StringBuilder generateQueryFromMethodName(QueryInfo queryInfo, boolean c orderBy = methodName.indexOf("OrderBy", by + 2); } parseFindClause(queryInfo, methodName, by > 0 ? by : orderBy > 0 ? orderBy : -1); - q = generateSelectClause(queryInfo); + q = queryInfo.generateSelectClause().append(" FROM ").append(entityInfo.name).append(' ').append(o); if (by > 0) { int where = q.length(); generateWhereClause(queryInfo, methodName, by + 2, orderBy > 0 ? orderBy : methodName.length(), q); @@ -1594,7 +1596,7 @@ private StringBuilder generateQueryFromMethodName(QueryInfo queryInfo, boolean c " repository method."); // TODO NLS queryInfo.type = QueryInfo.Type.FIND_AND_DELETE; parseDeleteBy(queryInfo, by); - q = generateSelectClause(queryInfo); + q = queryInfo.generateSelectClause().append(" FROM ").append(entityInfo.name).append(' ').append(o); queryInfo.jpqlDelete = generateDeleteById(queryInfo); } else { // DELETE queryInfo.type = queryInfo.type == null ? QueryInfo.Type.DELETE : queryInfo.type; @@ -1732,7 +1734,7 @@ private StringBuilder generateQueryFromMethodParams(QueryInfo queryInfo, Annotat } else if (methodTypeAnno instanceof Delete) { if (queryInfo.isFindAndDelete()) { queryInfo.type = QueryInfo.Type.FIND_AND_DELETE; - q = generateSelectClause(queryInfo); + q = queryInfo.generateSelectClause().append(" FROM ").append(entityInfo.name).append(' ').append(o); queryInfo.jpqlDelete = generateDeleteById(queryInfo); } else { // DELETE queryInfo.type = QueryInfo.Type.DELETE; @@ -1764,124 +1766,6 @@ private StringBuilder generateQueryFromMethodParams(QueryInfo queryInfo, Annotat return q; } - /** - * Generates the SELECT clause of the JPQL. - * - * @param queryInfo query information - * @return the SELECT clause. - */ - private StringBuilder generateSelectClause(QueryInfo queryInfo) { - StringBuilder q = new StringBuilder(200); - String o = queryInfo.entityVar; - String o_ = queryInfo.entityVar_; - EntityInfo entityInfo = queryInfo.entityInfo; - - String[] cols, selections = provider.compat.getSelections(queryInfo.method); - if (selections == null || selections.length == 0) { - cols = null; - } else if (queryInfo.type == QueryInfo.Type.FIND_AND_DELETE) { - // TODO NLS message for error path once selections are supported function - throw new UnsupportedOperationException(); - } else { - cols = new String[selections.length]; - for (int i = 0; i < cols.length; i++) { - String name = entityInfo.getAttributeName(selections[i], true); - cols[i] = name == null ? selections[i] : name; - } - } - - Class singleType = queryInfo.getSingleResultType(); - - if (singleType.isPrimitive()) - singleType = QueryInfo.wrapperClassIfPrimitive(singleType); - - q.append("SELECT "); - - if (cols == null || cols.length == 0) { - if (singleType.isAssignableFrom(entityInfo.entityClass) - || entityInfo.inheritance && entityInfo.entityClass.isAssignableFrom(singleType)) { - // Whole entity - q.append(o); - } else { - // Look for single entity attribute with the desired type: - String singleAttributeName = null; - for (Map.Entry> entry : entityInfo.attributeTypes.entrySet()) { - Class collectionElementType = entityInfo.collectionElementTypes.get(entry.getKey()); - Class attributeType = collectionElementType == null ? entry.getValue() : collectionElementType; - if (attributeType.isPrimitive()) - attributeType = QueryInfo.wrapperClassIfPrimitive(attributeType); - if (singleType.isAssignableFrom(attributeType)) { - singleAttributeName = entry.getKey(); - q.append(o_).append(singleAttributeName); - break; - } - } - - if (singleAttributeName == null) { - // Construct new instance from IdClass, embeddable, or entity attributes. - // It would be preferable if the spec included the Select annotation to explicitly identify parameters, but if that doesn't happen - // TODO we could compare attribute types with known constructor to improve on guessing a correct order of parameters - q.append("NEW ").append(singleType.getName()).append('('); - List relAttrNames; - boolean first = true; - if (entityInfo.idClassAttributeAccessors != null && singleType.equals(entityInfo.idType)) - for (String idClassAttributeName : entityInfo.idClassAttributeAccessors.keySet()) { - String name = entityInfo.getAttributeName(idClassAttributeName, true); - q.append(first ? "" : ", ").append(o).append('.').append(name); - first = false; - } - else if ((relAttrNames = entityInfo.relationAttributeNames.get(singleType)) != null) - for (String name : relAttrNames) { - q.append(first ? "" : ", ").append(o).append('.').append(name); - first = false; - } - else if (entityInfo.recordClass == null) - for (String name : entityInfo.attributeTypes.keySet()) { - q.append(first ? "" : ", ").append(o).append('.').append(name); - first = false; - } - else { - for (RecordComponent component : entityInfo.recordClass.getRecordComponents()) { - String name = component.getName(); - q.append(first ? "" : ", ").append(o).append('.').append(name); - first = false; - } - } - q.append(')'); - } - } - } else { // Individual columns are requested by @Select - Class entityType = entityInfo.getType(); - boolean selectAsColumns = singleType.isAssignableFrom(entityType) - || singleType.isInterface() // NEW instance doesn't apply to interfaces - || singleType.isPrimitive() // NEW instance should not be used on primitives - || singleType.getName().startsWith("java") // NEW instance constructor is unlikely for non-user-defined classes - || entityInfo.inheritance && entityType.isAssignableFrom(singleType); - if (!selectAsColumns && cols.length == 1) { - String singleAttributeName = cols[0]; - Class attributeType = entityInfo.collectionElementTypes.get(singleAttributeName); - if (attributeType == null) - attributeType = entityInfo.attributeTypes.get(singleAttributeName); - selectAsColumns = attributeType != null && (Object.class.equals(attributeType) // JPA metamodel does not preserve the type if not an EmbeddableCollection - || singleType.isAssignableFrom(attributeType)); - } - if (selectAsColumns) { - // Specify columns without creating new instance - for (int i = 0; i < cols.length; i++) - q.append(i == 0 ? "" : ", ").append(o).append('.').append(cols[i]); - } else { - // Construct new instance from defined columns - q.append("NEW ").append(singleType.getName()).append('('); - for (int i = 0; i < cols.length; i++) - q.append(i == 0 ? "" : ", ").append(o).append('.').append(cols[i]); - q.append(')'); - } - } - - q.append(" FROM ").append(entityInfo.name).append(' ').append(o); - return q; - } - /** * Generates the JPQL UPDATE clause for a repository updateBy method such as updateByProductIdSetProductNameMultiplyPrice */ diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/cdi/DataExtension.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/cdi/DataExtension.java index 8cb15e5a608..aae2177474f 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/cdi/DataExtension.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/cdi/DataExtension.java @@ -204,7 +204,7 @@ public void afterBeanDiscovery(@Observes AfterBeanDiscovery event, BeanManager b EntityManagerFactory emf = instance.get(); if (emBuilder == null) - emBuilder = new PUnitEMBuilder(emf, loader); + emBuilder = new PUnitEMBuilder(emf, loader, provider); else throw new UnsupportedOperationException// ("The " + method.getName() + " resource accessor method of the " + @@ -225,7 +225,7 @@ public void afterBeanDiscovery(@Observes AfterBeanDiscovery event, BeanManager b try { Object resource = InitialContext.doLookup(dataStore); if (resource instanceof EntityManagerFactory) - emBuilder = new PUnitEMBuilder((EntityManagerFactory) resource, dataStore, loader); + emBuilder = new PUnitEMBuilder((EntityManagerFactory) resource, dataStore, loader, provider); if (trace && tc.isDebugEnabled()) Tr.debug(this, tc, dataStore + " is the JNDI name for " + resource); @@ -238,7 +238,7 @@ public void afterBeanDiscovery(@Observes AfterBeanDiscovery event, BeanManager b Object resource = InitialContext.doLookup(javaCompName); if (resource instanceof EntityManagerFactory) - emBuilder = new PUnitEMBuilder((EntityManagerFactory) resource, javaCompName, loader); + emBuilder = new PUnitEMBuilder((EntityManagerFactory) resource, javaCompName, loader, provider); if (emBuilder != null || resource instanceof DataSource) { isJNDIName = true; diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/provider/PUnitEMBuilder.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/provider/PUnitEMBuilder.java index 4a1dca5e641..1ce665cab3d 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/provider/PUnitEMBuilder.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/provider/PUnitEMBuilder.java @@ -20,6 +20,7 @@ import com.ibm.ws.threadContext.ComponentMetaDataAccessorImpl; import io.openliberty.data.internal.persistence.EntityManagerBuilder; +import io.openliberty.data.internal.persistence.cdi.DataExtensionProvider; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; @@ -37,8 +38,8 @@ public class PUnitEMBuilder extends EntityManagerBuilder { private final String persistenceUnitRef; - public PUnitEMBuilder(EntityManagerFactory emf, ClassLoader repositoryClassLoader) { - super(repositoryClassLoader); + public PUnitEMBuilder(EntityManagerFactory emf, ClassLoader repositoryClassLoader, DataExtensionProvider provider) { + super(provider, repositoryClassLoader); this.emf = emf; this.persistenceUnitRef = emf.toString(); @@ -52,8 +53,9 @@ public PUnitEMBuilder(EntityManagerFactory emf, ClassLoader repositoryClassLoade // DataStoreTestApp#DataStoreTestWeb.war, org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl@3708cabf] } - public PUnitEMBuilder(EntityManagerFactory emf, String persistenceUnitRef, ClassLoader repositoryClassLoader) { - super(repositoryClassLoader); + public PUnitEMBuilder(EntityManagerFactory emf, String persistenceUnitRef, + ClassLoader repositoryClassLoader, DataExtensionProvider provider) { + super(provider, repositoryClassLoader); this.emf = emf; this.persistenceUnitRef = persistenceUnitRef; diff --git a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/service/DBStoreEMBuilder.java b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/service/DBStoreEMBuilder.java index f92689d6cbe..e60cbfd2c15 100644 --- a/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/service/DBStoreEMBuilder.java +++ b/dev/io.openliberty.data.internal.persistence/src/io/openliberty/data/internal/persistence/service/DBStoreEMBuilder.java @@ -121,7 +121,7 @@ public class DBStoreEMBuilder extends EntityManagerBuilder { public DBStoreEMBuilder(String dataStore, boolean isConfigDisplayId, boolean isJNDIName, AnnotatedType type, ClassLoader repositoryClassLoader, DataExtensionProvider provider) { - super(repositoryClassLoader); + super(provider, repositoryClassLoader); final boolean trace = TraceComponent.isAnyTracingEnabled(); ComponentMetaData cData = ComponentMetaDataAccessorImpl.getComponentMetaDataAccessor().getComponentMetaData(); diff --git a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java index 9ec580aa09e..1bfb1c642de 100644 --- a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java +++ b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/DataTestServlet.java @@ -3592,7 +3592,11 @@ public void testRecordInFromClause() { assertEquals(20.98f, receipts.totalOf(2000L), 0.001f); assertEquals(15.99f, receipts.totalOf(2001L), 0.001f); - receipts.deleteByTotalLessThan(2000.0f); + assertEquals(true, receipts.addTax(2001L, 0.0813f)); + + assertEquals(17.29f, receipts.totalOf(2001L), 0.001f); + + assertEquals(2, receipts.removeIfTotalUnder(2000.0f)); } /** diff --git a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Receipts.java b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Receipts.java index f3f72595d3f..c6ce0825b3d 100644 --- a/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Receipts.java +++ b/dev/io.openliberty.data.internal_fat/test-applications/DataTestApp/src/test/jakarta/data/web/Receipts.java @@ -26,6 +26,9 @@ */ @Repository public interface Receipts extends CrudRepository { + @Query("UPDATE Receipt SET total = total * (1.0 + :taxRate) WHERE purchaseId = :id") + boolean addTax(long id, float taxRate); + @Query("SELECT COUNT(this)") long count(); @@ -42,6 +45,9 @@ public interface Receipts extends CrudRepository { Stream findByPurchaseIdIn(Iterable ids); + @Query("DELETE FROM Receipt WHERE total < :max") + int removeIfTotalUnder(float max); + @Query("SELECT total FROM Receipt WHERE purchaseId=:id") float totalOf(long id); } diff --git a/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/DataJPATestServlet.java b/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/DataJPATestServlet.java index db723b4c4e4..f76b4afff68 100644 --- a/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/DataJPATestServlet.java +++ b/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/DataJPATestServlet.java @@ -2458,6 +2458,66 @@ public void testParenthesisInsertionForCursorPagination() { assertEquals(false, page3.hasNext()); } + /** + * Use a repository method that runs a query without specifying an entity type + * and returns a record entity. The repository must be able to infer the record type + * to use from the return value and generate the proper select clause so that the + * generated entity type is converted to the record type. + */ + @Test + public void testRecordQueryInfersSelectClause() { + + Rebate r1 = new Rebate(10, 10.00, "testRecordEntityInferredFromReturnType-CustomerA", // + LocalTime.of(15, 40, 0), // + LocalDate.of(2024, Month.MAY, 1), // + Rebate.Status.PAID, // + LocalDateTime.of(2024, Month.MAY, 1, 15, 40, 0), // + null); + + Rebate r2 = new Rebate(12, 12.00, "testRecordEntityInferredFromReturnType-CustomerA", // + LocalTime.of(12, 46, 30), // + LocalDate.of(2024, Month.APRIL, 5), // + Rebate.Status.PAID, // + LocalDateTime.of(2024, Month.MAY, 2, 10, 18, 0), // + null); + + Rebate r3 = new Rebate(13, 3.00, "testRecordEntityInferredFromReturnType-CustomerB", // + LocalTime.of(9, 15, 0), // + LocalDate.of(2024, Month.MAY, 2), // + Rebate.Status.PAID, // + LocalDateTime.of(2024, Month.MAY, 2, 9, 15, 0), // + null); + + Rebate r4 = new Rebate(14, 4.00, "testRecordEntityInferredFromReturnType-CustomerA", // + LocalTime.of(10, 55, 0), // + LocalDate.of(2024, Month.MAY, 1), // + Rebate.Status.VERIFIED, // + LocalDateTime.of(2024, Month.MAY, 2, 14, 27, 45), // + null); + + Rebate r5 = new Rebate(15, 5.00, "testRecordEntityInferredFromReturnType-CustomerA", // + LocalTime.of(17, 50, 0), // + LocalDate.of(2024, Month.MAY, 1), // + Rebate.Status.PAID, // + LocalDateTime.of(2024, Month.MAY, 5, 15, 5, 0), // + null); + + Rebate[] all = rebates.addAll(r1, r2, r3, r4, r5); + + List paid = rebates.paidTo("testRecordEntityInferredFromReturnType-CustomerA"); + + assertEquals(paid.toString(), 3, paid.size()); + Rebate r; + r = paid.get(0); + assertEquals(12.0f, r.amount(), 0.001); + r = paid.get(1); + assertEquals(10.0f, r.amount(), 0.001); + r = paid.get(2); + assertEquals(5.0f, r.amount(), 0.001); + + rebates.removeAll(all); + } + /** * Tests lifecycle methods returning a single record. */ diff --git a/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/Rebates.java b/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/Rebates.java index 509c4c602f7..fc01812a0ce 100644 --- a/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/Rebates.java +++ b/dev/io.openliberty.data.internal_fat_jpa/test-applications/DataJPATestApp/src/test/jakarta/data/jpa/web/Rebates.java @@ -18,6 +18,7 @@ import jakarta.data.repository.Delete; import jakarta.data.repository.Insert; +import jakarta.data.repository.Query; import jakarta.data.repository.Repository; import jakarta.data.repository.Save; import jakarta.data.repository.Update; @@ -45,6 +46,9 @@ public interface Rebates { // Do not allow this interface to inherit from other @Update List modifyMultiple(List r); + @Query("WHERE customerId=?1 AND status=test.jakarta.data.jpa.web.Rebate.Status.PAID ORDER BY amount DESC, id ASC") + List paidTo(String customerId); + @Save Rebate process(Rebate r);