Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 @@ -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
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 @@ -2890,6 +2890,32 @@ protected void appendPropertyProjection(QueryPropertyPath propertyPath) {
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());
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);
Comment thread
radovanradic marked this conversation as resolved.
Outdated
Comment thread
radovanradic marked this conversation as resolved.
Outdated
}
query.append(COMMA);
});
query.setLength(query.length() - 1);
return;
Comment thread
radovanradic marked this conversation as resolved.
Outdated
}

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,14 @@ 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
&& (type.hasStereotype(Embeddable.class)
|| type.hasStereotype("jakarta.persistence.Embeddable")
|| type.hasStereotype("javax.persistence.Embeddable"))) {
resultDataType = DataType.ENTITY;
}
Comment thread
radovanradic marked this conversation as resolved.
Outdated
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