Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ abstract class AbstractHibernateQuerySpec extends AbstractQuerySpec {
!found.isPresent()
}

void "test embedded audit projection retrieval"() {
when:
def id = UUID.randomUUID()
def saved = userWithWhereRepository.save(new UserWithWhere(id: id, email: "audit@somewhere.com", deleted: false))
def projectedAudit = userWithWhereRepository.findAuditById(id)
then:
saved
projectedAudit
projectedAudit.createdBy == "current"
projectedAudit.createdTime
cleanup:
userWithWhereRepository.deleteById(id)
}

void "test merge"() {
given:
studentRepository.deleteAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.jspecify.annotations.NonNull;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.hibernate.entities.Audit;
import io.micronaut.data.hibernate.entities.UserWithWhere;
import io.micronaut.data.model.Sort;
import io.micronaut.data.repository.CrudRepository;
Expand All @@ -21,5 +22,7 @@ public interface UserWithWhereRepository extends CrudRepository<UserWithWhere, U
@Query(value = "UPDATE users SET email = :email WHERE id = :id RETURNING email", nativeQuery = true)
String updateAndReturnEmail(String email, UUID id);

Audit findAuditById(UUID id);

void updateEmailById(UUID id, String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
package io.micronaut.data.jdbc.h2

import io.micronaut.data.tck.entities.Address
import io.micronaut.data.tck.entities.Jurisdiction
import io.micronaut.data.tck.entities.Registration
import io.micronaut.data.tck.entities.Restaurant
import io.micronaut.data.tck.entities.Vehicle
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Shared
import spock.lang.Specification
Expand All @@ -31,6 +34,10 @@ class H2EmbeddedSpec extends Specification {
@Shared
H2RestaurantRepository restaurantRepository

@Inject
@Shared
H2VehicleRepository vehicleRepository
Comment thread
radovanradic marked this conversation as resolved.

void "test save and retrieve entity with embedded"() {
when:"An entity is saved"
restaurantRepository.save(new Restaurant("Fred's Cafe", new Address("High St.", "7896")))
Expand Down Expand Up @@ -65,6 +72,16 @@ class H2EmbeddedSpec extends Specification {
restaurant.address.zipCode == '1234'
restaurant.hqAddress == null

when:"Embedded field is projected as return type"
def address = restaurantRepository.findAddressById(restaurant.id)
def hqAddress = restaurantRepository.findHqAddressById(restaurant.id).orElse(null)

then:"Address projection contains all fields and nullable hq projection is null"
address
address.street == 'Smith St.'
address.zipCode == '1234'
hqAddress == null

when:"The object is updated with non-null value"
restaurant.hqAddress = new Address("John St.", "4567")
restaurantRepository.update(restaurant)
Expand All @@ -76,11 +93,80 @@ class H2EmbeddedSpec extends Specification {
restaurant.hqAddress
restaurant.hqAddress.street == "John St."

when:"Nullable embedded field is projected after it is set"
hqAddress = restaurantRepository.findHqAddressById(restaurant.id).orElse(null)

then:"Projected nullable embedded field contains all fields"
hqAddress
hqAddress.street == "John St."
hqAddress.zipCode == "4567"

when:"A query is done by an embedded object"
restaurant = restaurantRepository.findByAddress(restaurant.address)

then:"The correct query is executed"
restaurant.address.street == 'Smith St.'
}

void "test save and retrieve nested embedded projections"() {
given:"A vehicle with two embedded registrations and nested jurisdictions"
def firstJurisdiction = new Jurisdiction()
firstJurisdiction.countryCode = "US"
firstJurisdiction.regionCode = "CA"
def firstRegistration = new Registration()
firstRegistration.plateNumber = "ABC-123"
firstRegistration.status = "ACTIVE"
firstRegistration.jurisdiction = firstJurisdiction

def secondJurisdiction = new Jurisdiction()
secondJurisdiction.countryCode = "CA"
secondJurisdiction.regionCode = "ON"
def secondRegistration = new Registration()
secondRegistration.plateNumber = "XYZ-789"
secondRegistration.status = "EXPIRED"
secondRegistration.jurisdiction = secondJurisdiction

def vehicle = new Vehicle()
vehicle.name = "Delivery Van"
vehicle.firstRegistration = firstRegistration
vehicle.secondRegistration = secondRegistration

when:"The vehicle is saved and embedded values are projected back"
vehicle = vehicleRepository.save(vehicle)
def projectedFirstRegistration = vehicleRepository.findFirstRegistrationById(vehicle.id)
def projectedSecondRegistration = vehicleRepository.findSecondRegistrationById(vehicle.id)
def projectedFirstJurisdiction = vehicleRepository.findFirstRegistrationJurisdictionById(vehicle.id)
def projectedSecondJurisdiction = vehicleRepository.findSecondRegistrationJurisdictionById(vehicle.id)
def criteriaFirstRegistration = vehicleRepository.findOne(H2VehicleRepository.Specifications.findFirstRegistrationById(vehicle.id))

then:"Top-level embedded projections contain nested embedded values"
projectedFirstRegistration
projectedFirstRegistration.plateNumber == "ABC-123"
projectedFirstRegistration.status == "ACTIVE"
projectedFirstRegistration.jurisdiction
projectedFirstRegistration.jurisdiction.countryCode == "US"
projectedFirstRegistration.jurisdiction.regionCode == "CA"

criteriaFirstRegistration
criteriaFirstRegistration.plateNumber == "ABC-123"
criteriaFirstRegistration.status == "ACTIVE"
criteriaFirstRegistration.jurisdiction
criteriaFirstRegistration.jurisdiction.countryCode == "US"
criteriaFirstRegistration.jurisdiction.regionCode == "CA"

projectedSecondRegistration
projectedSecondRegistration.plateNumber == "XYZ-789"
projectedSecondRegistration.status == "EXPIRED"
projectedSecondRegistration.jurisdiction
projectedSecondRegistration.jurisdiction.countryCode == "CA"
projectedSecondRegistration.jurisdiction.regionCode == "ON"

and:"Nested embedded field can be projected directly"
projectedFirstJurisdiction
projectedFirstJurisdiction.countryCode == "US"
projectedFirstJurisdiction.regionCode == "CA"
projectedSecondJurisdiction
projectedSecondJurisdiction.countryCode == "CA"
projectedSecondJurisdiction.regionCode == "ON"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.jdbc.h2;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
import io.micronaut.data.repository.jpa.criteria.CriteriaQueryBuilder;
import io.micronaut.data.tck.entities.Jurisdiction;
import io.micronaut.data.tck.entities.Registration;
import io.micronaut.data.tck.entities.Vehicle;

@JdbcRepository(dialect = Dialect.H2)
public interface H2VehicleRepository extends CrudRepository<Vehicle, Long>, JpaSpecificationExecutor<Vehicle> {

Registration findFirstRegistrationById(Long id);

Registration findSecondRegistrationById(Long id);

Jurisdiction findFirstRegistrationJurisdictionById(Long id);

Jurisdiction findSecondRegistrationJurisdictionById(Long id);

class Specifications {
static CriteriaQueryBuilder<Registration> findFirstRegistrationById(Long id) {
return criteriaBuilder -> {
var query = criteriaBuilder.createQuery(Registration.class);
var root = query.from(Vehicle.class);
query.select(root.get("firstRegistration")).where(criteriaBuilder.equal(root.get("id"), id));
return query;
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1662,7 +1662,7 @@
*/
@Internal
protected final class QueryState implements PropertyParameterCreator {
private final AbstractSqlLikeQueryBuilder.QueryBuilder queryBuilder;
private final QueryBuilder queryBuilder;
@Nullable
private final String rootAlias;
private final Map<String, JoinPath> appliedJoinPaths = new LinkedHashMap<>();
Expand All @@ -1672,11 +1672,11 @@
private final PersistentEntity entity;
private List<JoinPath> joinPaths = new ArrayList<>();

private QueryState(AbstractSqlLikeQueryBuilder.QueryBuilder queryBuilder, BaseQueryDefinition query, boolean allowJoins, boolean useAlias) {
private QueryState(QueryBuilder queryBuilder, BaseQueryDefinition query, boolean allowJoins, boolean useAlias) {
this(queryBuilder, query, allowJoins, useAlias, null);
}

private QueryState(AbstractSqlLikeQueryBuilder.QueryBuilder queryBuilder, BaseQueryDefinition query, boolean allowJoins, boolean useAlias, @Nullable String tableAliasPrefix) {
private QueryState(QueryBuilder queryBuilder, BaseQueryDefinition query, boolean allowJoins, boolean useAlias, @Nullable String tableAliasPrefix) {
this.queryBuilder = queryBuilder;
this.allowJoins = allowJoins;
this.baseQueryDefinition = query;
Expand Down Expand Up @@ -2966,6 +2966,8 @@
}
if (property instanceof Association association && !property.isEmbedded()) {
appendAssociationProjection(new PersistentAssociationPath(propertyPath.getAssociations(), association));
} else if (computePropertyPaths() && property instanceof Embedded) {
appendEmbeddedPropertyProjection(propertyPath);
} else {
appendPropertyProjection(findProperty(propertyPath.getPath()));
}
Expand Down Expand Up @@ -3199,6 +3201,81 @@
selectAllColumnsFromJoinPaths(queryState.baseQueryDefinition().getJoinPaths(), null);
}

/**
* Appends an embedded projection with column aliases matching the projected embeddable type.
*
* @param propertyPath The property path
*/
private void appendEmbeddedPropertyProjection(PersistentPropertyPath propertyPath) {
PersistentProperty property = propertyPath.getProperty();
if (!(property instanceof Embedded embedded)) {
appendPropertyProjection(findProperty(propertyPath.getPath()));
return;
}

boolean escape = shouldEscape(propertyPath.findPropertyOwner().orElse(property.getOwner()));
NamingStrategy sourceNamingStrategy = getNamingStrategy(propertyPath);
NamingStrategy targetNamingStrategy = getNamingStrategy(embedded.getAssociatedEntity());
List<Association> projectedPath = new ArrayList<>(propertyPath.getAssociations());
projectedPath.add(embedded);
int[] propertiesCount = new int[1];
PersistentEntityUtils.traversePersistentProperties(propertyPath, traverseEmbedded(), (associations, p) -> propertiesCount[0]++);
if (StringUtils.isNotEmpty(columnAlias) && propertiesCount[0] > 1) {
throw new IllegalStateException("Cannot apply a column alias: " + columnAlias + " with expanded property: " + propertyPath);
}
PersistentEntityUtils.traversePersistentProperties(propertyPath, traverseEmbedded(), (associations, p) -> {
List<Association> relativeAssociations = getRelativeAssociations(projectedPath, associations);
String targetName = StringUtils.isNotEmpty(columnAlias)
? columnAlias
: getEmbeddedProjectionTargetName(targetNamingStrategy, relativeAssociations, p);
appendEmbeddedProjectionProperty(associations, p, sourceNamingStrategy, queryState.rootAlias, escape, targetName);
query.append(COMMA);
});
if (propertiesCount[0] > 0) {
query.setLength(query.length() - 1);
}
}

private List<Association> getRelativeAssociations(List<Association> projectedPath, List<Association> associations) {
if (associations.size() <= projectedPath.size()) {
return Collections.emptyList();
}
return associations.subList(projectedPath.size(), associations.size());
}

private String getEmbeddedProjectionTargetName(NamingStrategy targetNamingStrategy,
List<Association> relativeAssociations,
PersistentProperty property) {
String columnAlias = getColumnAlias(property);
if (StringUtils.isNotEmpty(columnAlias)) {
return columnAlias;
}
return getMappedName(targetNamingStrategy, relativeAssociations, property);
}

private void appendEmbeddedProjectionProperty(List<Association> associations,
PersistentProperty property,
NamingStrategy sourceNamingStrategy,
@Nullable String tableAlias,
boolean escape,
String targetName) {
String transformed = getDataTransformerReadValue(tableAlias, property).orElse(null);
if (transformed != null) {
query.append(transformed).append(AS_CLAUSE).append(targetName);
return;
}
String column = getMappedName(sourceNamingStrategy, associations, property);
String escapedColumn = escapeColumnIfNeeded(column, escape);
if (tableAlias == null) {
query.append(escapedColumn);
} else {
query.append(tableAlias).append(DOT).append(escapedColumn);
}
if (!column.equals(targetName)) {
query.append(AS_CLAUSE).append(targetName);
}
}

/**
* Append the property projection.
*
Expand Down Expand Up @@ -3339,7 +3416,7 @@
String tableAlias,
boolean escape) {
String transformed = getDataTransformerReadValue(tableAlias, property).orElse(null);
String columnAlias = getColumnAlias(property);

Check warning on line 3419 in data-model/src/main/java/io/micronaut/data/model/query/builder/sql/AbstractSqlLikeQueryBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "columnAlias" which hides the field declared at line 2930.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-data&issues=AZ50bamZq2fm5lfmm_0L&open=AZ50bamZq2fm5lfmm_0L&pullRequest=3774
boolean useAlias = StringUtils.isNotEmpty(columnAlias);
if (transformed != null) {
sb.append(transformed).append(AS_CLAUSE).append(useAlias ? columnAlias : property.getPersistedName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc
methodInfo.getOperationType(),
queryResult,
methodInfo.getResultType(),
methodInfo.getResultDataType(),
parameterBinding,
methodInfo.isEncodeEntityParameters(),
methodInfo.isOptimisticLock());
Expand All @@ -654,6 +655,7 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc
queryDefinition.operationType(),
additionalQueryResult,
queryDefinition.resultType(),
methodInfo.getResultDataType(),
additionalParameterBinding,
methodInfo.isEncodeEntityParameters(),
queryDefinition.optimisticLock());
Expand Down Expand Up @@ -685,6 +687,7 @@ private void processMethodInfo(MethodMatchContext methodMatchContext, MethodMatc
DataMethod.OperationType.COUNT,
countQuery,
methodMatchContext.getVisitorContext().getClassElement(Long.class).orElseThrow(),
null,
countParametersBindings,
methodInfo.isEncodeEntityParameters(),
false);
Expand All @@ -701,6 +704,8 @@ private void addQueryDefinition(MethodMatchContext methodMatchContext,
QueryResult queryResult,
@Nullable
TypedElement resultType,
@Nullable
DataType resultDataType,
List<QueryParameterBinding> parameterBinding,
boolean encodeEntityParameters,
boolean optimisticLock) {
Expand All @@ -722,7 +727,8 @@ private void addQueryDefinition(MethodMatchContext methodMatchContext,
annotationBuilder.member(DataMethodQuery.META_MEMBER_RESULT_TYPE, new AnnotationClassValue<>(stringType));
ClassElement type = resultType.getType();
if (!TypeUtils.isVoid(type)) {
annotationBuilder.member(DataMethodQuery.META_MEMBER_RESULT_DATA_TYPE, TypeUtils.resolveDataType(type, dataTypes));
annotationBuilder.member(DataMethodQuery.META_MEMBER_RESULT_DATA_TYPE,
resultDataType == null ? TypeUtils.resolveDataType(type, dataTypes) : resultDataType);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Introspected;
import org.jspecify.annotations.Nullable;
import io.micronaut.data.annotation.Embeddable;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.RepositoryConfiguration;
import io.micronaut.data.processor.visitors.MatchFailedException;
Expand Down Expand Up @@ -89,7 +90,10 @@ private static boolean isIsDto(ClassElement repositoryClass,
}

public static boolean isDto(ClassElement entityType, ClassElement resultType) {
return resultType.hasStereotype(Introspected.class) && entityType.hasStereotype(MappedEntity.class)
if (resultType.hasAnnotation(Embeddable.class) || resultType.hasStereotype(Embeddable.class)) {
return false;
}
return (resultType.hasStereotype(Introspected.class) && entityType.hasStereotype(MappedEntity.class))
|| isObjectArrayResult(resultType); // Allow Object[] as a DTO
}

Expand Down
Loading
Loading