Using CriteriaBuilder with embedded IDs
Expected Behavior
I'm trying to write an update criteria that updates a row with a composite key. An abbreviated version of the classes involved:
@Embeddable
data class MemberProfileId(
@field:MappedProperty("family_id") val familyId: String,
@field:MappedProperty("member_id") val memberId: String
)
@MappedEntity
data class MemberProfileDto(
@field:EmbeddedId val id: MemberProfileId,
@field:MappedProperty("given_name") val givenName: String? = null,
// more fields omitted
)
And I'm trying to write an UpdateSpecification<MemberProfileDto>:
override fun toPredicate(
root: Root<MemberProfileDto>, query: CriteriaUpdate<*>, criteriaBuilder: CriteriaBuilder
): Predicate? {
givenName?.let { query.set(root.get("givenName"), it) }
// update more fields conditionally
return criteriaBuilder.equals(root.get<MemberProfileId>("id"), targetId)
}
Actual Behaviour
From logs it looks like the SQL generated is correct, but the arguments bound are not:
2024-09-14T14:19:34.695Z [reactor-tcp-nio-14] DEBUG io.micronaut.data.query - Executing Query: UPDATE "member_profile" SET "given_name"=$1 WHERE ("family_id" = $2 AND "member_id" = $3) []
2024-09-14T14:19:34.695Z [reactor-tcp-nio-14] TRACE io.micronaut.data.query - Binding parameter at position 0 to value foo2 with data type: STRING []
2024-09-14T14:19:34.695Z [reactor-tcp-nio-14] TRACE io.micronaut.data.query - Binding parameter at position 1 to value MemberProfileId(familyId=MOb9rKxVrTQX_mUs6P8YN, memberId=pV5H2vV1d8kdtPp0LTIA1) with data type: STRING []
2024-09-14T14:19:34.695Z [reactor-tcp-nio-14] TRACE io.micronaut.data.query - Binding parameter at position 2 to value MemberProfileId(familyId=MOb9rKxVrTQX_mUs6P8YN, memberId=pV5H2vV1d8kdtPp0LTIA1) with data type: STRING []
The bound values appear to just be toString of the MemberProfileId class, not the individual components.
I've tried a fair number of things to try working around this, but nothing has been successful so far. Is there a hook into the conversion to string I could use to perform the correct value binding?
I saw a discussion about a similar issue from > 2 years ago, with no answer. I'm figuring this is a bug, or that I've missed something.
Steps To Reproduce
- Attempt to write an update query with CriteriaBuilder that refers to an embedded ID.
Environment Information
- Micronaut 4.6.1
- Micronaut-data 4.9.3
- r2dbc 1.0.0-RELEASE, r2dbc-postgresql 1.0.5.RELEASE
Example Application
No response
Version
4.6.1
I did hack out a workaround by implementing a (terrible, ugly) PersistentPropertyPath:
package mypackage
import io.micronaut.data.model.Association
import io.micronaut.data.model.PersistentEntity
import io.micronaut.data.model.PersistentProperty
import io.micronaut.data.model.jpa.criteria.PersistentEntityPath
import io.micronaut.data.model.jpa.criteria.PersistentPropertyPath
import io.micronaut.data.model.jpa.criteria.impl.ExpressionVisitor
import jakarta.persistence.criteria.Expression
import jakarta.persistence.criteria.Path
import jakarta.persistence.criteria.Root
import jakarta.persistence.metamodel.Bindable
import jakarta.persistence.metamodel.MapAttribute
import jakarta.persistence.metamodel.PluralAttribute
import jakarta.persistence.metamodel.SingularAttribute
internal class BasicPersistentPropertyPath<T>(
val name: String,
private val alias: String,
private val type: Class<T>,
private val parent: Root<*>
) : PersistentPropertyPath<T> {
override fun getJavaType(): Class<out T> = type
override fun getModel(): Bindable<T> {
TODO("Not yet implemented")
}
override fun getParentPath(): Path<*> = parent
override fun <Y : Any?> get(attributeName: String?): Path<Y> {
TODO("Not yet implemented")
}
override fun type(): Expression<Class<out T>> {
TODO("Not yet implemented")
}
override fun visitExpression(expressionVisitor: ExpressionVisitor?) {
TODO("Not yet implemented")
}
override fun getProperty(): PersistentProperty = object : PersistentProperty {
override fun getName(): String = [email protected]
override fun getPersistedName(): String = [email protected]
override fun getTypeName(): String {
TODO("Not yet implemented")
}
override fun getOwner(): PersistentEntity {
return (parent as PersistentEntityPath<*>).persistentEntity ?:
throw IllegalStateException("No owner found for property: $name")
}
override fun isAssignable(type: String?): Boolean {
TODO("Not yet implemented")
}
}
override fun getAssociations(): MutableList<Association> = mutableListOf()
override fun <K : Any?, V : Any?, M : MutableMap<K, V>?> get(map: MapAttribute<T, K, V>?): Expression<M> {
TODO("Not yet implemented")
}
override fun <E : Any?, C : MutableCollection<E>?> get(collection: PluralAttribute<T, C, E>?): Expression<C> {
TODO("Not yet implemented")
}
override fun <Y : Any?> get(attribute: SingularAttribute<in T, Y>?): Path<Y> {
TODO("Not yet implemented")
}
}
Usage in my case:
return criteriaBuilder.and(
criteriaBuilder.equal(
BasicPersistentPropertyPath<String>("familyId", "family_id", String::class.java, root),
id.familyId),
criteriaBuilder.equal(BasicPersistentPropertyPath<String>(
"memberId", "member_id", String::class.java, root),
id.memberId)
)
I'm having a similar issue and a similar workaround sort of works for me when querying entities from a JDBCRepository using JpaSpecificationExecutor.findAll. Extending your example this works:
PredicateSpecification familyPredicate = new PredicateSpecification<MemberProfileDto>() {
@Override
public @Nullable Predicate toPredicate(@NonNull Root< MemberProfileDto > root, @NonNull CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(new BasicPeristentPropertyPath<>("familyId", "family_id", String.class, root), id.familyId;
}
};
@NonNull List<MemberProfileDto> members = memberRepo.findAll(familyPredicate);
However when I try to invoke the variant of findAll that takes a Page (or CursoredPage) I get errors.
memberRepo.findAll(familyPredicate, Pageable.from(1, 10));
10:42:48.464 [io-executor-thread-1] ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Embedded are not allowed as an expression!
java.lang.IllegalArgumentException: Embedded are not allowed as an expression!
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2.appendPropertyRef(AbstractSqlLikeQueryBuilder2.java:1284)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2.appendExpression(AbstractSqlLikeQueryBuilder2.java:1264)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2$SqlSelectionVisitor.appendExpression(AbstractSqlLikeQueryBuilder2.java:2906)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2$SqlSelectionVisitor.appendFunction(AbstractSqlLikeQueryBuilder2.java:2894)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2$SqlSelectionVisitor.appendFunction(AbstractSqlLikeQueryBuilder2.java:2886)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2$SqlSelectionVisitor.visit(AbstractSqlLikeQueryBuilder2.java:2608)
at io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression.visitExpression(UnaryExpression.java:62)
at io.micronaut.data.model.jpa.criteria.IExpression.visitSelection(IExpression.java:101)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2.buildSelect(AbstractSqlLikeQueryBuilder2.java:433)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2.buildSelectClause(AbstractSqlLikeQueryBuilder2.java:365)
at io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2.buildQuery(AbstractSqlLikeQueryBuilder2.java:227)
at io.micronaut.data.model.query.builder.sql.SqlQueryBuilder2.buildSelect(SqlQueryBuilder2.java:1478)
at io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityQuery.buildQuery(AbstractPersistentEntityQuery.java:104)
at io.micronaut.data.model.jpa.criteria.impl.QueryResultPersistentEntityCriteriaQuery.buildQuery(QueryResultPersistentEntityCriteriaQuery.java:39)
at io.micronaut.data.runtime.intercept.criteria.AbstractPreparedQueryCriteriaRepositoryOperations.buildFind(AbstractPreparedQueryCriteriaRepositoryOperations.java:136)
at io.micronaut.data.runtime.intercept.criteria.AbstractPreparedQueryCriteriaRepositoryOperations.createFindOne(AbstractPreparedQueryCriteriaRepositoryOperations.java:110)
at io.micronaut.data.runtime.intercept.criteria.PreparedQueryCriteriaRepositoryOperations.findOne(PreparedQueryCriteriaRepositoryOperations.java:68)
at io.micronaut.data.runtime.intercept.criteria.FindPageSpecificationInterceptor.intercept(FindPageSpecificationInterceptor.java:86)
at io.micronaut.data.runtime.intercept.DataIntroductionAdvice.intercept(DataIntroductionAdvice.java:84)
at io.micronaut.aop.chain.MethodInterceptorChain.proceed(MethodInterceptorChain.java:143)
at com.iss.datadesk.clientassets.ephemeral.EphemeralAssetsRepository$Intercepted.findAll(Unknown Source)
@radovanradic I was wondering if you had any thoughts on if what I am seeing is a separate issue entirely, if it's related the workaround above being incomplete, if it belongs as part of the fix for this issue, or if it's related to this issue you fixed recently?
Does not look like related to the recently fixed issue. Whether it is the same issue, not sure. Maybe the root cause is the same (maybe @dstepanov can figure out if can be fixed the same way. I tried one approach to fix this issue but it wasn't accepted as correct approach) but from the exception does not look the same.