micronaut-data icon indicating copy to clipboard operation
micronaut-data copied to clipboard

Using CriteriaBuilder with embedded IDs

Open casey-marshall-yolabs opened this issue 1 year ago • 3 comments

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

  1. 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

casey-marshall-yolabs avatar Sep 14 '24 14:09 casey-marshall-yolabs

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)
)

casey-marshall-yolabs avatar Sep 14 '24 15:09 casey-marshall-yolabs

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?

cutz avatar May 16 '25 15:05 cutz

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.

radovanradic avatar May 18 '25 18:05 radovanradic