diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java index 6d3c435be276..11d7f16c8aef 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java @@ -86,7 +86,6 @@ else if ( method.equals( setIdentifierMethod ) ) { // otherwise: return INVOKE_IMPLEMENTATION; - } private Object getReplacement() { diff --git a/hibernate-core/src/main/java/org/hibernate/query/assignment/Assignment.java b/hibernate-core/src/main/java/org/hibernate/query/assignment/Assignment.java new file mode 100644 index 000000000000..c2da2f7b971f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/assignment/Assignment.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.assignment; + + +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.Incubating; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; + +/** + * An assignment to a field or property of an entity or embeddable. + * + * @param The target entity type of the assignment + * + * @since 7.2 + * + * @author Gavin King + */ +@Incubating +public interface Assignment { + + /** + * An assigment of the given literal value to the given attribute + * of the root entity. + */ + static Assignment set(SingularAttribute attribute, X value) { + return new AttributeAssignment<>( attribute, value ); + } + + /** + * An assigment of the given literal value to the entity or embeddable + * field or property identified by the given path from the root entity. + */ + static Assignment set(Path path, X value) { + return new PathAssignment<>( path, value ); + } + + /** + * An assigment of the entity or embeddable field or property identified + * by the given path from the root entity to the given attribute of the + * root entity. + */ + static Assignment set(SingularAttribute attribute, Path value) { + return new PathToAttributeAssignment<>( attribute, value ); + } + + /** + * An assigment of one entity or embeddable field or property to another + * entity or embeddable field or property, each identified by a given path + * from the root entity. + */ + static Assignment set(Path path, Path value) { + return new PathToPathAssignment<>( path, value ); + } + + void apply(SqmUpdateStatement update); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/assignment/AttributeAssignment.java b/hibernate-core/src/main/java/org/hibernate/query/assignment/AttributeAssignment.java new file mode 100644 index 000000000000..7909f459180f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/assignment/AttributeAssignment.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.assignment; + +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; + +/** + * Assignment of a value to an attribute. + * + * @author Gavin King + */ +record AttributeAssignment(SingularAttribute attribute, X value) + implements Assignment { + @Override + public void apply(SqmUpdateStatement update) { + update.set( attribute, value ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/assignment/PathAssignment.java b/hibernate-core/src/main/java/org/hibernate/query/assignment/PathAssignment.java new file mode 100644 index 000000000000..6f1daa2e0333 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/assignment/PathAssignment.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.assignment; + +import org.hibernate.query.restriction.Path; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; + +/** + * Assignment of a value to a path. + * + * @author Gavin King + */ +record PathAssignment(Path path, X value) + implements Assignment { + @Override + public void apply(SqmUpdateStatement update) { + update.set( path.path( update.getRoot() ), value ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/assignment/PathToAttributeAssignment.java b/hibernate-core/src/main/java/org/hibernate/query/assignment/PathToAttributeAssignment.java new file mode 100644 index 000000000000..0b67baf1da60 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/assignment/PathToAttributeAssignment.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.assignment; + +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; + +/** + * Assignment of a path to an attribute. + * + * @author Gavin King + */ +record PathToAttributeAssignment(SingularAttribute attribute, Path value) + implements Assignment { + @Override + public void apply(SqmUpdateStatement update) { + update.set( attribute, value.path( update.getRoot() ) ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/assignment/PathToPathAssignment.java b/hibernate-core/src/main/java/org/hibernate/query/assignment/PathToPathAssignment.java new file mode 100644 index 000000000000..f3b5a2890ad3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/assignment/PathToPathAssignment.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.assignment; + +import org.hibernate.query.restriction.Path; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; + +/** + * * Assignment of a path to a path. + * + * @author Gavin King + */ +record PathToPathAssignment(Path path, Path value) + implements Assignment { + @Override + public void apply(SqmUpdateStatement update) { + update.set( path.path( update.getRoot() ), value.path( update.getRoot() ) ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/assignment/package-info.java b/hibernate-core/src/main/java/org/hibernate/query/assignment/package-info.java new file mode 100644 index 000000000000..0348b429fcdb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/assignment/package-info.java @@ -0,0 +1,10 @@ +/** + * Support for {@linkplain org.hibernate.query.assignment.Assignment assignment} + * with {@link org.hibernate.query.specification.MutationSpecification}. + * + * @since 7.2 + */ +@Incubating +package org.hibernate.query.assignment; + +import org.hibernate.Incubating; diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/DeleteSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/specification/DeleteSpecification.java new file mode 100644 index 000000000000..a665d507979c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/DeleteSpecification.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import org.hibernate.Incubating; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.internal.DeleteSpecificationImpl; + +/** + * Specialization of {@link MutationSpecification} for programmatic customization + * of delete queries. + *

+ * + * @param The entity type which is the target of the mutation. + * + * @author Gavin King + * + * @since 7.2 + */ +@Incubating +public interface DeleteSpecification extends MutationSpecification { + @Override + DeleteSpecification restrict(Restriction restriction); + + @Override + DeleteSpecification augment(Augmentation augmentation); + + @Override + DeleteSpecification validate(CriteriaBuilder builder); + + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain org.hibernate.query.MutationQuery} which + * deletes the given entity type. + * + * @param targetEntityClass The target entity type + * + * @param The root entity type for the mutation (the "target"). + */ + static DeleteSpecification create(Class targetEntityClass) { + return new DeleteSpecificationImpl<>( targetEntityClass ); + } + + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain org.hibernate.query.MutationQuery} based + * on the given criteria delete, allowing the addition of + * {@linkplain #restrict restrictions}. + * + * @param criteriaDelete The criteria delete query + * + * @param The root entity type for the mutation (the "target"). + */ + static DeleteSpecification create(CriteriaDelete criteriaDelete) { + return new DeleteSpecificationImpl<>( criteriaDelete ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/MutationSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/specification/MutationSpecification.java index 0b5105a295d8..f97ffe4fd7e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/MutationSpecification.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/MutationSpecification.java @@ -114,6 +114,8 @@ static MutationSpecification create(Class mutationTarget, String hql) * @param criteriaUpdate The criteria update query * * @param The root entity type for the mutation (the "target"). + * + * @see UpdateSpecification#create(CriteriaUpdate) */ static MutationSpecification create(CriteriaUpdate criteriaUpdate) { return new MutationSpecificationImpl<>( criteriaUpdate ); @@ -127,6 +129,8 @@ static MutationSpecification create(CriteriaUpdate criteriaUpdate) { * @param criteriaDelete The criteria delete query * * @param The root entity type for the mutation (the "target"). + * + * @see DeleteSpecification#create(CriteriaDelete) */ static MutationSpecification create(CriteriaDelete criteriaDelete) { return new MutationSpecificationImpl<>( criteriaDelete ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/ProjectionSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/specification/ProjectionSpecification.java new file mode 100644 index 000000000000..9c3e868d4b2c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/ProjectionSpecification.java @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.Incubating; +import org.hibernate.Session; +import org.hibernate.StatelessSession; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.specification.internal.ProjectionSpecificationImpl; + +import java.util.function.Function; + +/** + * Allows a {@link SelectionSpecification} to be augmented with the specification + * of a projection list. + *

+ * var specification =
+ *         SelectionSpecification.create(Book.class)
+ *                 .restrict(Restriction.contains(Book_.title, "hibernate", false))
+ *                 .sort(Order.desc(Book_.title));
+ * var projection = ProjectionSpecification.create(specification);
+ * var bookIsbn = projection.select(Book_.isbn);
+ * var bookTitle = projection.select(Book_.title);
+ * var results = projection.createQuery(session).getResultList();
+ * for (var result : results) {
+ *     var isbn = bookIsbn.in(result);
+ *     var title = bookTitle.in(result);
+ *     ...
+ * }
+ * 
+ *

+ * A {@code ProjectionSpecification} always results in a query which with result + * type {@code Object[]}. The {@link #select(SingularAttribute) select()} methods + * return {@link Element}, allowing easy and typesafe access to the elements of + * the returned array. + * + * @param The result type of the {@link SelectionSpecification} + * + * @since 7.2 + * + * @apiNote This interface marked {@link Incubating} is considered experimental. + * Changes to the API defined here are fully expected in future releases. + * + * @author Gavin King + */ +@Incubating +public interface ProjectionSpecification extends QuerySpecification { + + /** + * Create a new {@code ProjectionSpecification} which augments the given + * {@link SelectionSpecification}. + */ + static ProjectionSpecification create(SelectionSpecification selectionSpecification) { + return new ProjectionSpecificationImpl<>( selectionSpecification ); + } + + /** + * Allows typesafe access to elements of the {@code Object[]} + * arrays returned by the query. + * + * @param The type of the element of the projection list + */ + @FunctionalInterface + interface Element extends Function { + X in(Object[] tuple); + + @Override + default X apply(Object[] tuple) { + return in(tuple); + } + } + + /** + * Select the given attribute of the root entity. + * + * @param attribute An attribute of the root entity + * @return An {@link Element} allowing typesafe access to the results + */ + Element select(SingularAttribute attribute); + + /** + * Select the given field or property identified by the given path + * from of the root entity. + * + * @param path A path from the root entity + * @return An {@link Element} allowing typesafe access to the results + */ + Element select(Path path); + + @Override + SelectionQuery createQuery(Session session); + + @Override + SelectionQuery createQuery(StatelessSession session); + + @Override + SelectionQuery createQuery(EntityManager entityManager); + + @Override + CriteriaQuery buildCriteria(CriteriaBuilder builder); + + @Override + TypedQueryReference reference(); + + @Override + ProjectionSpecification validate(CriteriaBuilder builder); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/SelectionSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/specification/SelectionSpecification.java index c64ddc652b16..3c7fca9b8bf8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/SelectionSpecification.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/SelectionSpecification.java @@ -4,11 +4,12 @@ */ package org.hibernate.query.specification; -import jakarta.persistence.TypedQueryReference; import jakarta.persistence.EntityManager; -import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.TypedQueryReference; import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; +import jakarta.persistence.metamodel.SingularAttribute; import org.hibernate.Incubating; import org.hibernate.Session; @@ -17,9 +18,9 @@ import org.hibernate.query.Order; import org.hibernate.query.Page; import org.hibernate.query.SelectionQuery; -import org.hibernate.query.specification.internal.SelectionSpecificationImpl; import org.hibernate.query.restriction.Path; import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.internal.SelectionSpecificationImpl; import java.util.List; @@ -242,4 +243,50 @@ static SelectionSpecification create(Class resultType, String hql) { static SelectionSpecification create(CriteriaQuery criteria) { return new SelectionSpecificationImpl<>( criteria ); } + + /** + * Create a {@link ProjectionSpecification} allowing selection of specific + * {@linkplain ProjectionSpecification#select(SingularAttribute) fields} + * and {@linkplain ProjectionSpecification#select(Path) compound paths}. + * The returned projection holds a reference to this specification, + * and so mutation of this object also affects the projection. + * + * @return a new {@link ProjectionSpecification} + * + * @since 7.2 + */ + @Incubating + default ProjectionSpecification createProjection() { + return ProjectionSpecification.create( this ); + } + + /** + * Create a {@link SimpleProjectionSpecification} for the given + * {@linkplain ProjectionSpecification#select(SingularAttribute) field}. + * The returned projection holds a reference to this specification, + * and so mutation of this object also affects the projection. + * + * @return a new {@link SimpleProjectionSpecification} + * + * @since 7.2 + */ + @Incubating + default SimpleProjectionSpecification createProjection(SingularAttribute attribute) { + return SimpleProjectionSpecification.create( this, attribute ); + } + + /** + * Create a {@link SimpleProjectionSpecification} for the given + * {@linkplain ProjectionSpecification#select(Path) compound path}. + * The returned projection holds a reference to this specification, + * and so mutation of this object also affects the projection. + * + * @return a new {@link SimpleProjectionSpecification} + * + * @since 7.2 + */ + @Incubating + default SimpleProjectionSpecification createProjection(Path path) { + return SimpleProjectionSpecification.create( this, path ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/SimpleProjectionSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/specification/SimpleProjectionSpecification.java new file mode 100644 index 000000000000..819364dba20e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/SimpleProjectionSpecification.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.Incubating; +import org.hibernate.Session; +import org.hibernate.StatelessSession; + +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.specification.internal.SimpleProjectionSpecificationImpl; + +/** + * Allows a {@link SelectionSpecification} to be augmented with the specification of + * a single projected {@linkplain SingularAttribute attribute} or {@linkplain Path path}. + *

+ * var specification =
+ *         SelectionSpecification.create(Book.class)
+ *                 .restrict(Restriction.contains(Book_.title, "hibernate", false))
+ *                 .sort(Order.desc(Book_.title));
+ * var projection = SimpleProjectionSpecification.create(specification, Book_.isbn);
+ * var isbns = projection.createQuery(session).getResultList();
+ * 
+ *

+ * Use of a {@link Path} allows joining to associated entities. + *

+ * var specification =
+ *         SelectionSpecification.create(Book.class)
+ *                 .restrict(Restriction.contains(Book_.title, "hibernate", false))
+ *                 .sort(Order.desc(Book_.title));
+ * var projection =
+ *         SimpleProjectionSpecification.create(specification,
+ *                 Path.from(Book.class)
+ *                     .to(Book_.publisher)
+ *                     .to(Publisher_.name));
+ * var publisherNames = projection.createQuery(session).getResultList();
+ * 
+ * + * @param The result type of the {@link SelectionSpecification} + * @param The type of the projected path or attribute + * + * @since 7.2 + * + * @apiNote This interface marked {@link Incubating} is considered experimental. + * Changes to the API defined here are fully expected in future releases. + * + * @author Gavin King + */ +@Incubating +public interface SimpleProjectionSpecification extends QuerySpecification { + /** + * Create a new {@code ProjectionSpecification} which augments the given + * {@link SelectionSpecification}. + */ + static SimpleProjectionSpecification create( + SelectionSpecification selectionSpecification, + Path projectedPath) { + return new SimpleProjectionSpecificationImpl<>( selectionSpecification, projectedPath ); + } + + /** + * Create a new {@code ProjectionSpecification} which augments the given + * {@link SelectionSpecification}. + */ + static SimpleProjectionSpecification create( + SelectionSpecification selectionSpecification, + SingularAttribute projectedAttribute) { + return new SimpleProjectionSpecificationImpl<>( selectionSpecification, projectedAttribute ); + } + + @Override + SelectionQuery createQuery(Session session); + + @Override + SelectionQuery createQuery(StatelessSession session); + + @Override + SelectionQuery createQuery(EntityManager entityManager); + + @Override + CriteriaQuery buildCriteria(CriteriaBuilder builder); + + @Override + TypedQueryReference reference(); + + @Override + SimpleProjectionSpecification validate(CriteriaBuilder builder); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/UpdateSpecification.java b/hibernate-core/src/main/java/org/hibernate/query/specification/UpdateSpecification.java new file mode 100644 index 000000000000..fba7493dfa95 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/UpdateSpecification.java @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import org.hibernate.Incubating; +import org.hibernate.query.assignment.Assignment; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.internal.UpdateSpecificationImpl; + +import java.util.List; + +/** + * Specialization of {@link MutationSpecification} for programmatic customization + * of update queries. + *

+ * The method {@link #assign(Assignment)} allows application of additional + * {@linkplain Assignment assignments} to the mutated entity. The static factory + * methods of {@link Assignment} are used to express assignments to attributes + * or compound paths. + *

+ * UpdateSpecification.create(Book.class)
+ *         .assign(Assignment.set(Book_.title, newTitle))
+ *         .restrict(Restriction.equal(Book_.isbn, isbn))
+ *         .createQuery(session)
+ *         .executeUpdate();
+ * 
+ * + * @param The entity type which is the target of the mutation. + * + * @author Gavin King + * + * @since 7.2 + */ +@Incubating +public interface UpdateSpecification extends MutationSpecification { + /** + * Add an assigment to a field or property of the target entity. + * + * @param assignment The assignment to add + * + * @return {@code this} for method chaining. + */ + UpdateSpecification assign(Assignment assignment); + + /** + * Sets the assignments to fields or properties of the target entity. + * If assignments were already specified, this method drops the previous + * assignments in favor of the passed {@code assignments}. + * + * @param assignments The new assignments + * + * @return {@code this} for method chaining. + */ + UpdateSpecification reassign(List> assignments); + + @Override + UpdateSpecification restrict(Restriction restriction); + + @Override + UpdateSpecification augment(Augmentation augmentation); + + @Override + UpdateSpecification validate(CriteriaBuilder builder); + + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain org.hibernate.query.MutationQuery} which + * updates the given entity type. + * + * @param targetEntityClass The target entity type + * + * @param The root entity type for the mutation (the "target"). + */ + static UpdateSpecification create(Class targetEntityClass) { + return new UpdateSpecificationImpl<>( targetEntityClass ); + } + + /** + * Returns a specification reference which can be used to programmatically, + * iteratively build a {@linkplain org.hibernate.query.MutationQuery} based + * on the given criteria update, allowing the addition of + * {@linkplain #restrict restrictions} and {@linkplain #assign assignments}. + * + * @param criteriaUpdate The criteria update query + * + * @param The root entity type for the mutation (the "target"). + */ + static UpdateSpecification create(CriteriaUpdate criteriaUpdate) { + return new UpdateSpecificationImpl<>( criteriaUpdate ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/DeleteSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/DeleteSpecificationImpl.java new file mode 100644 index 000000000000..0fd0db0bfb6d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/DeleteSpecificationImpl.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification.internal; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.DeleteSpecification; + +/** + * @author Gavin King + */ +public class DeleteSpecificationImpl + extends MutationSpecificationImpl + implements DeleteSpecification { + + public DeleteSpecificationImpl(Class mutationTarget) { + super( MutationType.DELETE, mutationTarget ); + } + + public DeleteSpecificationImpl(CriteriaDelete criteriaQuery) { + super( criteriaQuery ); + } + +@Override + public DeleteSpecification restrict(Restriction restriction) { + super.restrict( restriction ); + return this; + } + + @Override + public DeleteSpecification augment(Augmentation augmentation) { + super.augment( augmentation ); + return this; + } + + @Override + public DeleteSpecification validate(CriteriaBuilder builder) { + super.validate( builder ); + return this; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java index a9db084bd2be..41b6de50500f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java @@ -48,27 +48,45 @@ */ public class MutationSpecificationImpl implements MutationSpecification, TypedQueryReference { - private final List, SqmRoot>> specifications = new ArrayList<>(); + public enum MutationType { +// INSERT, + UPDATE, + DELETE + } + + final List, SqmRoot>> specifications = new ArrayList<>(); + private final String hql; private final Class mutationTarget; private final SqmDeleteOrUpdateStatement deleteOrUpdateStatement; + private final MutationType type; public MutationSpecificationImpl(String hql, Class mutationTarget) { this.hql = hql; this.mutationTarget = mutationTarget; this.deleteOrUpdateStatement = null; + this.type = null; } public MutationSpecificationImpl(CriteriaUpdate criteriaQuery) { this.deleteOrUpdateStatement = (SqmUpdateStatement) criteriaQuery; this.mutationTarget = deleteOrUpdateStatement.getTarget().getManagedType().getJavaType(); this.hql = null; + this.type = MutationType.UPDATE; } public MutationSpecificationImpl(CriteriaDelete criteriaQuery) { this.deleteOrUpdateStatement = (SqmDeleteStatement) criteriaQuery; this.mutationTarget = deleteOrUpdateStatement.getTarget().getManagedType().getJavaType(); this.hql = null; + this.type = MutationType.DELETE; + } + + public MutationSpecificationImpl(MutationType type, Class mutationTarget) { + this.deleteOrUpdateStatement = null; + this.mutationTarget = mutationTarget; + this.hql = null; + this.type = type; } @Override @@ -137,6 +155,14 @@ else if ( deleteOrUpdateStatement != null ) { mutationTargetRoot = resolveSqmRoot( sqmStatement, sqmStatement.getTarget().getManagedType().getJavaType() ); } + else if ( type != null ) { + final var criteriaBuilder = queryEngine.getCriteriaBuilder(); + sqmStatement = switch ( type ) { + case UPDATE -> criteriaBuilder.createCriteriaUpdate( mutationTarget ); + case DELETE -> criteriaBuilder.createCriteriaDelete( mutationTarget ); + }; + mutationTargetRoot = sqmStatement.getTarget(); + } else { throw new AssertionFailure( "No HQL or criteria" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/ProjectionSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/ProjectionSpecificationImpl.java new file mode 100644 index 000000000000..61954c799819 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/ProjectionSpecificationImpl.java @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification.internal; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.Session; +import org.hibernate.SharedSessionContract; +import org.hibernate.StatelessSession; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.ProjectionSpecification; +import org.hibernate.query.specification.QuerySpecification; +import org.hibernate.query.specification.SelectionSpecification; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; +import org.hibernate.query.sqm.tree.select.SqmSelection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +/** + * @author Gavin King + */ +public class ProjectionSpecificationImpl implements ProjectionSpecification, TypedQueryReference { + + private final SelectionSpecification selectionSpecification; + private final List, SqmRoot, SqmSelectableNode>> specifications = new ArrayList<>(); + + public ProjectionSpecificationImpl(SelectionSpecification selectionSpecification) { + this.selectionSpecification = selectionSpecification; + } + + @Override + public Element select(SingularAttribute attribute) { + final int position = specifications.size(); + specifications.add( (select, root) -> root.get( attribute ) ); + return tuple -> (X) tuple[position]; + } + + @Override + public Element select(Path path) { + final int position = specifications.size(); + specifications.add( (select, root) -> (SqmPath) path.path( root ) ); + return tuple -> (X) tuple[position]; + } + + @Override + public QuerySpecification restrict(Restriction restriction) { + throw new UnsupportedOperationException( "This is not supported yet!" ); + } + + @Override + public SelectionQuery createQuery(Session session) { + return session.createSelectionQuery( buildCriteria( session.getCriteriaBuilder() ) ); + } + + @Override + public SelectionQuery createQuery(StatelessSession session) { + return session.createSelectionQuery( buildCriteria( session.getCriteriaBuilder() ) ); + } + + @Override + public SelectionQuery createQuery(EntityManager entityManager) { + return entityManager.unwrap( SharedSessionContract.class ) + .createQuery( buildCriteria( entityManager.getCriteriaBuilder() ) ); + } + + @Override + public CriteriaQuery buildCriteria(CriteriaBuilder builder) { + var impl = (SelectionSpecificationImpl) selectionSpecification; + // TODO: handle HQL, existing criteria + final var tupleQuery = + (SqmSelectStatement) + builder.createQuery(Object[].class); + final var root = tupleQuery.from( impl.getResultType() ); + // This cast is completely bogus + final var castStatement = (SqmSelectStatement) tupleQuery; + impl.getSpecifications().forEach( spec -> spec.accept( castStatement, root ) ); + final var nodeBuilder = (NodeBuilder) builder; + final var selectClause = tupleQuery.getQuerySpec().getSelectClause(); + for ( int i = 0; i < specifications.size(); i++ ) { + final var selection = specifications.get( i ).apply( tupleQuery, root ); + selectClause.addSelection( new SqmSelection<>( selection, selection.getAlias(), nodeBuilder ) ); + } + return tupleQuery; + } + + @Override + public ProjectionSpecification validate(CriteriaBuilder builder) { + selectionSpecification.validate( builder ); + // TODO: validate projection + return this; + } + + @Override + public TypedQueryReference reference() { + return this; + } + + @Override + public String getName() { + return null; + } + + @Override + public Class getResultType() { + return Object[].class; + } + + @Override + public Map getHints() { + return Collections.emptyMap(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java index 990085005ec6..7995c7215e61 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java @@ -90,6 +90,10 @@ public TypedQueryReference reference() { return this; } + public List, SqmRoot>> getSpecifications() { + return specifications; + } + @Override public SelectionSpecification restrict(Restriction restriction) { specifications.add( (sqmStatement, root) -> { diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SimpleProjectionSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SimpleProjectionSpecificationImpl.java new file mode 100644 index 000000000000..b1680fc74861 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SimpleProjectionSpecificationImpl.java @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification.internal; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQueryReference; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.Session; +import org.hibernate.SharedSessionContract; +import org.hibernate.StatelessSession; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.QuerySpecification; +import org.hibernate.query.specification.SelectionSpecification; +import org.hibernate.query.specification.SimpleProjectionSpecification; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; + +import java.util.Collections; +import java.util.Map; + +/** + * @author Gavin King + */ +public class SimpleProjectionSpecificationImpl implements SimpleProjectionSpecification, TypedQueryReference { + + private final SelectionSpecification selectionSpecification; + private final Path path; + private final SingularAttribute attribute; + + public SimpleProjectionSpecificationImpl(SelectionSpecification specification, Path path) { + this.selectionSpecification = specification; + this.path = path; + this.attribute = null; + } + + public SimpleProjectionSpecificationImpl(SelectionSpecification specification, SingularAttribute attribute) { + this.selectionSpecification = specification; + this.attribute = attribute; + this.path = null; + } + + @Override + public QuerySpecification restrict(Restriction restriction) { + throw new UnsupportedOperationException( "This is not supported yet!" ); + } + + @Override + public SelectionQuery createQuery(Session session) { + return session.createSelectionQuery( buildCriteria( session.getCriteriaBuilder() ) ); + } + + @Override + public SelectionQuery createQuery(StatelessSession session) { + return session.createSelectionQuery( buildCriteria( session.getCriteriaBuilder() ) ); + } + + @Override + public SelectionQuery createQuery(EntityManager entityManager) { + return entityManager.unwrap( SharedSessionContract.class ) + .createQuery( buildCriteria( entityManager.getCriteriaBuilder() ) ); + } + + @Override + public CriteriaQuery buildCriteria(CriteriaBuilder builder) { + var impl = (SelectionSpecificationImpl) selectionSpecification; + // TODO: handle HQL, existing criteria + final var tupleQuery = + (SqmSelectStatement) + builder.createQuery( getResultType() ); + final var root = tupleQuery.from( impl.getResultType() ); + // This cast is completely bogus + final var castStatement = (SqmSelectStatement) tupleQuery; + impl.getSpecifications().forEach( spec -> spec.accept( castStatement, root ) ); + if ( path != null ) { + tupleQuery.select( path.path( root ) ); + } + else if ( attribute != null ) { + tupleQuery.select( root.get( attribute ) ); + } + return tupleQuery; + } + + @Override + public SimpleProjectionSpecification validate(CriteriaBuilder builder) { + selectionSpecification.validate( builder ); + // TODO: validate projection + return this; + } + + @Override + public TypedQueryReference reference() { + return this; + } + + @Override + public String getName() { + return null; + } + + @Override + public Class getResultType() { + if ( path != null ) { + return path.getType(); + } + else if ( attribute != null ) { + return attribute.getJavaType(); + } + else { + throw new IllegalStateException( "No path or attribute" ); + } + } + + @Override + public Map getHints() { + return Collections.emptyMap(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/UpdateSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/UpdateSpecificationImpl.java new file mode 100644 index 000000000000..a2ce2f6e82dc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/UpdateSpecificationImpl.java @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.specification.internal; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import org.hibernate.query.assignment.Assignment; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.UpdateSpecification; +import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; + +import java.util.List; + +/** + * @author Gavin King + */ +public class UpdateSpecificationImpl + extends MutationSpecificationImpl + implements UpdateSpecification { + + public UpdateSpecificationImpl(Class mutationTarget) { + super( MutationType.UPDATE, mutationTarget ); + } + + public UpdateSpecificationImpl(CriteriaUpdate criteriaQuery) { + super( criteriaQuery ); + } + + @Override + public UpdateSpecification assign(Assignment assignment) { + specifications.add( (sqmStatement, mutationTargetRoot) -> { + if ( sqmStatement instanceof SqmUpdateStatement sqmUpdateStatement ) { + assignment.apply( sqmUpdateStatement ); + } + else { + throw new IllegalStateException( "Delete query cannot perform assignment" ); + } + } ); + return this; + } + + @Override + public UpdateSpecification reassign(List> assignments) { + specifications.add( (sqmStatement, mutationTargetRoot) -> { + if ( sqmStatement instanceof SqmUpdateStatement sqmUpdateStatement ) { + final var setClause = sqmUpdateStatement.getSetClause(); + if ( setClause != null ) { + setClause.clearAssignments(); + } + assignments.forEach( assignment -> assignment.apply( sqmUpdateStatement ) ); + } + else { + throw new IllegalStateException( "Delete query cannot perform assignment" ); + } + } ); + return this; + } + + @Override + public UpdateSpecification restrict(Restriction restriction) { + super.restrict( restriction ); + return this; + } + + @Override + public UpdateSpecification augment(Augmentation augmentation) { + super.augment( augmentation ); + return this; + } + + @Override + public UpdateSpecification validate(CriteriaBuilder builder) { + super.validate( builder ); + return this; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java index c31f7ad5aae5..4f65ccf5044a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmSetClause.java @@ -5,7 +5,6 @@ package org.hibernate.query.sqm.tree.update; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -15,6 +14,8 @@ import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.expression.SqmExpression; +import static java.util.Collections.unmodifiableList; + /** * @author Steve Ebersole */ @@ -38,7 +39,11 @@ public SqmSetClause copy(SqmCopyContext context) { } public List> getAssignments() { - return Collections.unmodifiableList( assignments ); + return unmodifiableList( assignments ); + } + + public void clearAssignments() { + assignments.clear(); } public void addAssignment(SqmAssignment assignment) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/assignment/AssignmentTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/assignment/AssignmentTest.java new file mode 100644 index 000000000000..8c73328676af --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/assignment/AssignmentTest.java @@ -0,0 +1,219 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.assignment; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.query.assignment.Assignment; +import org.hibernate.query.restriction.Path; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.DeleteSpecification; +import org.hibernate.query.specification.SelectionSpecification; +import org.hibernate.query.specification.SimpleProjectionSpecification; +import org.hibernate.query.specification.UpdateSpecification; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SuppressWarnings("JUnitMalformedDeclaration") +@SessionFactory +@DomainModel(annotatedClasses = AssignmentTest.Book.class) +public class AssignmentTest { + + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist(new Book("9781932394153", "Hibernate in Action")); + session.persist(new Book("9781617290459", "Java Persistence with Hibernate")); + }); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.dropData(); + } + + @Test void testAssignment(SessionFactoryScope scope) { + var bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType(Book.class); + assertNotNull( bookType ); + @SuppressWarnings("unchecked") + var title = + (SingularAttribute) + bookType.findSingularAttribute("title"); + @SuppressWarnings("unchecked") + var isbn = + (SingularAttribute) + bookType.findSingularAttribute("isbn"); + scope.inTransaction(session -> { + int rows = + UpdateSpecification.create( Book.class ) + .assign( Assignment.set( title, "Hibernate In Action!" ) ) + .restrict( Restriction.equal( isbn, "9781932394153" ) ) + .createQuery( session ) + .executeUpdate(); + assertEquals( 1, rows ); + assertEquals( "Hibernate In Action!", + session.find( Book.class, "9781932394153" ).title ); + }); + } + + @Test void testPathAssignment(SessionFactoryScope scope) { + var bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType(Book.class); + assertNotNull( bookType ); + @SuppressWarnings("unchecked") + var title = + (SingularAttribute) + bookType.findSingularAttribute("title"); + @SuppressWarnings("unchecked") + var isbn = + (SingularAttribute) + bookType.findSingularAttribute("isbn"); + scope.inTransaction(session -> { + int rows = + UpdateSpecification.create( Book.class ) + .assign( Assignment.set( Path.from(Book.class).to(title), + "Hibernate In Action!!!" ) ) + .restrict( Restriction.equal( isbn, "9781932394153" ) ) + .createQuery( session ) + .executeUpdate(); + assertEquals( 1, rows ); + assertEquals( "Hibernate In Action!!!", + session.find( Book.class, "9781932394153" ).title ); + }); + } + + @Test void testAssignmentCriteria(SessionFactoryScope scope) { + var bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType(Book.class); + assertNotNull( bookType ); + @SuppressWarnings("unchecked") + var title = + (SingularAttribute) + bookType.findSingularAttribute("title"); + @SuppressWarnings("unchecked") + var isbn = + (SingularAttribute) + bookType.findSingularAttribute("isbn"); + scope.inTransaction(session -> { + var builder = session.getCriteriaBuilder(); + var query = builder.createCriteriaUpdate( Book.class ); + var root = query.from( Book.class ); + query.where( root.get( isbn ).equalTo( "9781932394153" ) ); + int rows = + UpdateSpecification.create( query ) + .assign( Assignment.set( title, "Hibernate in Action!" ) ) + .createQuery( session ) + .executeUpdate(); + assertEquals( 1, rows ); + assertEquals( "Hibernate in Action!", + session.find( Book.class, "9781932394153" ).title ); + }); + } + + @Test void testPathAssignmentCriteria(SessionFactoryScope scope) { + var bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType(Book.class); + assertNotNull( bookType ); + @SuppressWarnings("unchecked") + var title = + (SingularAttribute) + bookType.findSingularAttribute("title"); + @SuppressWarnings("unchecked") + var isbn = + (SingularAttribute) + bookType.findSingularAttribute("isbn"); + scope.inTransaction(session -> { + var builder = session.getCriteriaBuilder(); + var query = builder.createCriteriaUpdate( Book.class ); + var root = query.from( Book.class ); + query.where( root.get( isbn ).equalTo( "9781932394153" ) ); + int rows = + UpdateSpecification.create( query ) + .assign( Assignment.set( Path.from(Book.class).to(title), + "Hibernate in Action!!!" ) ) + .createQuery( session ) + .executeUpdate(); + assertEquals( 1, rows ); + assertEquals( "Hibernate in Action!!!", + session.find( Book.class, "9781932394153" ).title ); + }); + } + + @Test void testDelete(SessionFactoryScope scope) { + var bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType(Book.class); + assertNotNull( bookType ); + @SuppressWarnings("unchecked") + var title = + (SingularAttribute) + bookType.findSingularAttribute("title"); + @SuppressWarnings("unchecked") + var isbn = + (SingularAttribute) + bookType.findSingularAttribute("isbn"); + + scope.inTransaction( session -> { + DeleteSpecification.create( Book.class ) + .restrict( Restriction.startsWith( title, "Hibernate" ) ) + .createQuery( session ) + .executeUpdate(); + var list = + SimpleProjectionSpecification.create( SelectionSpecification.create( Book.class ), isbn ) + .createQuery( session ) + .getResultList(); + assertEquals( 1, list.size() ); + assertEquals( "9781617290459", list.get( 0 ) ); + } ); + + } + + @Test void testCriteriaDelete(SessionFactoryScope scope) { + var bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType(Book.class); + assertNotNull( bookType ); + @SuppressWarnings("unchecked") + var title = + (SingularAttribute) + bookType.findSingularAttribute("title"); + @SuppressWarnings("unchecked") + var isbn = + (SingularAttribute) + bookType.findSingularAttribute("isbn"); + + scope.inTransaction( session -> { + var builder = session.getCriteriaBuilder(); + var query = builder.createCriteriaDelete( Book.class ); + var root = query.from( Book.class ); + DeleteSpecification.create( query ) + .restrict( Restriction.startsWith( title, "Hibernate" ) ) + .createQuery( session ) + .executeUpdate(); + var list = + SimpleProjectionSpecification.create( SelectionSpecification.create( Book.class ), isbn ) + .createQuery( session ) + .getResultList(); + assertEquals( 1, list.size() ); + assertEquals( "9781617290459", list.get( 0 ) ); + } ); + + } + + @Entity(name="Book") + static class Book { + @Id String isbn; + String title; + + Book(String isbn, String title) { + this.isbn = isbn; + this.title = title; + } + + Book() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/OtherEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/OtherEntity.java index 4dcbe3e566b9..7b886257e3d9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/OtherEntity.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/OtherEntity.java @@ -12,5 +12,5 @@ @Table(name = "OtherEntity") public class OtherEntity { @Id - private Integer id; + Integer id; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/ProjectionSpecificationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/ProjectionSpecificationTest.java new file mode 100644 index 000000000000..fd938c449053 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/ProjectionSpecificationTest.java @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.dynamic; + +import org.hibernate.query.restriction.Path; +import org.hibernate.query.specification.SelectionSpecification; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = {BasicEntity.class, OtherEntity.class}) +@org.hibernate.testing.orm.junit.SessionFactory(useCollectingStatementInspector = true) +public class ProjectionSpecificationTest { + + @BeforeAll + public static void setup(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + session.createMutationQuery( "insert OtherEntity (id) values (69)" ) + .executeUpdate(); + session.createMutationQuery( "insert BasicEntity (id, name, position, other) values (1, 'Gavin', 2, (select o from OtherEntity o where o.id = 69))" ) + .executeUpdate(); + } ); + } + + @Test + void testProjection(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var spec = SelectionSpecification.create( BasicEntity.class ); + var projection = spec.createProjection(); + var position = projection.select( BasicEntity_.position ); + var name = projection.select( BasicEntity_.name ); + var id = projection.select( Path.from( BasicEntity.class ).to( BasicEntity_.id ) ); + var otherId = projection.select( Path.from( BasicEntity.class ).to( BasicEntity_.other ).to( OtherEntity_.id ) ); + var tuple = projection.createQuery( session ).getSingleResult(); + assertEquals( 2, position.in(tuple) ); + assertEquals( "Gavin", name.in(tuple) ); + assertEquals( 1, id.in(tuple) ); + assertEquals( 69, otherId.in( tuple ) ); + }); + } + + @Test + void testSimpleProjection(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var spec = SelectionSpecification.create( BasicEntity.class ); + var projection = spec.createProjection( BasicEntity_.name ); + var name = projection.createQuery( session ).getSingleResult(); + assertEquals( "Gavin", name ); + }); + } + + @Test + void testSimpleProjectionPath(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var spec = SelectionSpecification.create( BasicEntity.class ); + var projection = + spec.createProjection( + Path.from( BasicEntity.class) + .to( BasicEntity_.other ) + .to( OtherEntity_.id ) ); + var id = projection.createQuery( session ).getSingleResult(); + assertEquals( 69, id ); + }); + } + + @Test + void testSimpleEntityProjection(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var spec = SelectionSpecification.create( BasicEntity.class ); + var projection = spec.createProjection( BasicEntity_.other ); + var otherEntity = projection.createQuery( session ).getSingleResult(); + assertNotNull( otherEntity ); + assertEquals( 69, otherEntity.id ); + }); + } + + @Test + void testSimpleEntityProjectionPath(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + var spec = SelectionSpecification.create( BasicEntity.class ); + var projection = spec.createProjection( Path.from(BasicEntity.class).to(BasicEntity_.other) ); + var otherEntity = projection.createQuery( session ).getSingleResult(); + assertNotNull( otherEntity ); + assertEquals( 69, otherEntity.id ); + }); + } +}