Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.micronaut.data.runtime.operations.ExecutorAsyncOperations;
import io.micronaut.data.runtime.query.MethodContextAwareStoredQueryDecorator;
import io.micronaut.data.runtime.query.PreparedQueryDecorator;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

Expand Down Expand Up @@ -65,8 +66,9 @@ final class SyncCosmosRepositoryOperations implements
* @param reactiveCosmosRepositoryOperations The reactive cosmos repository operations
* @param executorService The executor service
*/
private SyncCosmosRepositoryOperations(DefaultReactiveCosmosRepositoryOperations reactiveCosmosRepositoryOperations,
@Named("io") @Nullable ExecutorService executorService) {
@Inject
SyncCosmosRepositoryOperations(DefaultReactiveCosmosRepositoryOperations reactiveCosmosRepositoryOperations,
@Named("io") @Nullable ExecutorService executorService) {
this.reactiveCosmosRepositoryOperations = reactiveCosmosRepositoryOperations;
this.executorService = executorService;
}
Expand Down
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

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 @@ -2869,7 +2869,7 @@
*
* @param propertyPath The property
*/
protected void appendPropertyProjection(QueryPropertyPath propertyPath) {

Check failure on line 2872 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

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-data&issues=AZ0gmkPo4h7RP8cXP9uP&open=AZ0gmkPo4h7RP8cXP9uP&pullRequest=3774

Check warning on line 2872 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

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 66 to 64, Complexity from 15 to 14, Nesting Level from 3 to 2, Number of Variables from 20 to 6.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-data&issues=AZ0k85VKYdn-O0VHedo4&open=AZ0k85VKYdn-O0VHedo4&pullRequest=3774
boolean jsonEntity = isJsonEntity(annotationMetadata, entity);
if (!computePropertyPaths() || jsonEntity) {
query.append(propertyPath.getTableAlias()).append(DOT);
Expand All @@ -2890,6 +2890,36 @@
String tableAlias = propertyPath.getTableAlias();
boolean escape = propertyPath.shouldEscape();
NamingStrategy namingStrategy = propertyPath.getNamingStrategy();

// Projection for Embeddable retrieval. EmbeddedId is covered in Id projection/traversal.
boolean isIdentityProperty = propertyPath.getProperty().getOwner().hasIdentity() && propertyPath.getProperty().getOwner().getIdentity() == propertyPath.getProperty();
if (propertyPath.getProperty() instanceof Association association && association.isEmbedded() && !isIdentityProperty) {
int resultAssociationOffset = propertyPath.getAssociations().size() + 1;
NamingStrategy resultNamingStrategy = getNamingStrategy(association.getAssociatedEntity());
boolean[] needsTrimming = {false};
PersistentEntityUtils.traversePersistentProperties(propertyPath.getAssociations(), propertyPath.getProperty(), traverseEmbedded(), (associations, property) -> {
String projectedColumnName = getMappedName(namingStrategy, associations, property);
// Nested embedded fields
List<Association> resultAssociations = associations.size() <= resultAssociationOffset
? Collections.emptyList()
: associations.subList(resultAssociationOffset, associations.size());
String resultColumnName = getMappedName(resultNamingStrategy, resultAssociations, property);
query
.append(tableAlias)
.append(DOT)
.append(escape ? quote(projectedColumnName) : projectedColumnName);
if (!projectedColumnName.equals(resultColumnName)) {
query.append(AS_CLAUSE).append(resultColumnName);
}
query.append(COMMA);
needsTrimming[0] = true;
});
if (needsTrimming[0]) {
query.setLength(query.length() - 1);
}
return;
}

boolean[] needsTrimming = {false};
int[] propertiesCount = new int[1];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.Delete;
import io.micronaut.data.annotation.Embeddable;
import io.micronaut.data.annotation.EntityRepresentation;
import io.micronaut.data.annotation.Insert;
import io.micronaut.data.annotation.Join;
Expand Down Expand Up @@ -680,7 +681,15 @@ 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));
DataType resultDataType = TypeUtils.resolveDataType(type, dataTypes);
if (operationType == DataMethod.OperationType.QUERY
&& resultDataType == DataType.OBJECT
&& (type.hasStereotype(Embeddable.class)
|| type.hasStereotype("jakarta.persistence.Embeddable")
|| type.hasStereotype("javax.persistence.Embeddable"))) {
resultDataType = DataType.ENTITY;
}
annotationBuilder.member(DataMethodQuery.META_MEMBER_RESULT_DATA_TYPE, resultDataType);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ interface BookRepository extends GenericRepository<Book, Long> {
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.GenericRepository;
import io.micronaut.data.tck.entities.Address;
import io.micronaut.data.tck.entities.Restaurant;
import java.util.Optional;

Expand All @@ -994,6 +995,10 @@ interface RestaurantRepository extends GenericRepository<Restaurant, Long> {

Restaurant findByAddressStreet(String street);

Address findAddressById(Long id);

Optional<Address> findHqAddressById(Long id);

String getMaxAddressStreetByName(String name);
}

Expand All @@ -1002,12 +1007,79 @@ interface RestaurantRepository extends GenericRepository<Restaurant, Long> {
def findByNameQuery = getQuery(repository.getRequiredMethod("findByName", String))
def saveQuery = getQuery(repository.getRequiredMethod("save", Restaurant))
def findByAddressStreetQuery = getQuery(repository.getRequiredMethod("findByAddressStreet", String))
def findAddressByIdMethod = repository.getRequiredMethod("findAddressById", Long)
def findAddressByIdQuery = getQuery(findAddressByIdMethod)
def findHqAddressByIdMethod = repository.getRequiredMethod("findHqAddressById", Long)
def findHqAddressByIdQuery = getQuery(findHqAddressByIdMethod)
def getMaxAddressStreetByNameQuery = getQuery(repository.getRequiredMethod("getMaxAddressStreetByName", String))
expect:
findByNameQuery == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`street`,restaurant_.`zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`name` = ?)'
saveQuery == 'INSERT INTO `restaurant` (`name`,`street`,`zip_code`,`hqaddress_street`,`hqaddress_zip_code`) VALUES (?,?,?,?,?)'
findByAddressStreetQuery == 'SELECT restaurant_.`id`,restaurant_.`name`,restaurant_.`street`,restaurant_.`zip_code`,restaurant_.`hqaddress_street`,restaurant_.`hqaddress_zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`street` = ?)'
findAddressByIdQuery == 'SELECT restaurant_.`street`,restaurant_.`zip_code` FROM `restaurant` restaurant_ WHERE (restaurant_.`id` = ?)'
findHqAddressByIdQuery == 'SELECT restaurant_.`hqaddress_street` AS street,restaurant_.`hqaddress_zip_code` AS zip_code FROM `restaurant` restaurant_ WHERE (restaurant_.`id` = ?)'
getMaxAddressStreetByNameQuery == 'SELECT MAX(restaurant_.`street`) FROM `restaurant` restaurant_ WHERE (restaurant_.`name` = ?)'
getResultDataType(findAddressByIdMethod) == DataType.ENTITY
getResultDataType(findHqAddressByIdMethod) == DataType.ENTITY
}

void "test invalid embedded projection result"() {
when:
buildRepository('test.RestaurantRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.GenericRepository;
import io.micronaut.data.tck.entities.Restaurant;
import io.micronaut.data.tck.entities.ShipmentId;
import java.util.Optional;
@JdbcRepository(dialect = Dialect.MYSQL)
interface RestaurantRepository extends GenericRepository<Restaurant, Long> {
Optional<ShipmentId> findAddressByName(String name);
}
""")
then:
Throwable ex = thrown()
ex.message.contains("method returns an incompatible type")
}

void "test nested embedded projection result"() {
given:
def repository = buildRepository('test.VehicleRepository', """
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.GenericRepository;
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)
interface VehicleRepository extends GenericRepository<Vehicle, Long> {

Registration findFirstRegistrationById(Long id);

Registration findSecondRegistrationById(Long id);

Jurisdiction findFirstRegistrationJurisdictionById(Long id);

Jurisdiction findSecondRegistrationJurisdictionById(Long id);
}

""")

def firstRegistrationMethod = repository.getRequiredMethod("findFirstRegistrationById", Long)
def secondRegistrationMethod = repository.getRequiredMethod("findSecondRegistrationById", Long)
def firstJurisdictionMethod = repository.getRequiredMethod("findFirstRegistrationJurisdictionById", Long)
def secondJurisdictionMethod = repository.getRequiredMethod("findSecondRegistrationJurisdictionById", Long)

expect:
getQuery(firstRegistrationMethod) == 'SELECT vehicle_.`plate_number`,vehicle_.`status`,vehicle_.`jurisdiction_country_code`,vehicle_.`jurisdiction_region_code` FROM `vehicle` vehicle_ WHERE (vehicle_.`id` = ?)'
getQuery(secondRegistrationMethod) == 'SELECT vehicle_.`second_plate_number` AS plate_number,vehicle_.`second_status` AS status,vehicle_.`second_jurisdiction_country_code` AS jurisdiction_country_code,vehicle_.`second_jurisdiction_region_code` AS jurisdiction_region_code FROM `vehicle` vehicle_ WHERE (vehicle_.`id` = ?)'
getQuery(firstJurisdictionMethod) == 'SELECT vehicle_.`jurisdiction_country_code` AS country_code,vehicle_.`jurisdiction_region_code` AS region_code FROM `vehicle` vehicle_ WHERE (vehicle_.`id` = ?)'
getQuery(secondJurisdictionMethod) == 'SELECT vehicle_.`second_jurisdiction_country_code` AS country_code,vehicle_.`second_jurisdiction_region_code` AS region_code FROM `vehicle` vehicle_ WHERE (vehicle_.`id` = ?)'
getResultDataType(firstRegistrationMethod) == DataType.ENTITY
getResultDataType(secondRegistrationMethod) == DataType.ENTITY
getResultDataType(firstJurisdictionMethod) == DataType.ENTITY
getResultDataType(secondJurisdictionMethod) == DataType.ENTITY
}

void "test count query with joins"() {
Expand Down
Loading
Loading