Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Left join returns incorrect number of records #2965

Merged
merged 8 commits into from
Jun 4, 2024
Merged
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 @@ -346,7 +346,7 @@ private <T, R> R findOne(Connection connection, SqlPreparedQuery<T, R> preparedQ
SqlTypeMapper<ResultSet, R> mapper = createMapper(preparedQuery, ResultSet.class);
R result;
if (mapper instanceof SqlResultEntityTypeMapper<ResultSet, R> entityTypeMapper) {
final boolean hasJoins = !preparedQuery.getJoinFetchPaths().isEmpty();
final boolean hasJoins = !preparedQuery.getJoinPaths().isEmpty();

SqlResultEntityTypeMapper.PushingMapper<ResultSet, R> oneMapper = entityTypeMapper.readOneMapper();
if (rs.next()) {
Expand Down Expand Up @@ -462,7 +462,7 @@ private <T, R> Stream<R> findStream(@NonNull PreparedQuery<T, R> pq, Connection
SqlResultConsumer<R> sqlMappingConsumer = preparedQuery.hasResultConsumer() ? preparedQuery.getParameterInRole(SqlResultConsumer.ROLE, SqlResultConsumer.class).orElse(null) : null;
SqlTypeMapper<ResultSet, R> resultMapper = createMapper(preparedQuery, ResultSet.class);
if (resultMapper instanceof SqlResultEntityTypeMapper<ResultSet, R> entityTypeMapper) {
Set<JoinPath> joinFetchPaths = preparedQuery.getJoinFetchPaths();
Set<JoinPath> joinFetchPaths = preparedQuery.getJoinPaths();
boolean onlySingleEndedJoins = isOnlySingleEndedJoins(persistentEntity, joinFetchPaths);
// Cannot stream ResultSet for "many" joined query
if (!onlySingleEndedJoins) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class H2EnumsMappingSpec extends Specification implements H2TestPropertyProvider
def sql = builder.buildBatchCreateTableStatement(PersistentEntity.of(EnumEntity))

then:
sql == 'CREATE TABLE `enum_entity` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`as_default` VARCHAR(255) NOT NULL,`as_string` VARCHAR(255) NOT NULL,`as_int` INT NOT NULL);'
sql == 'CREATE TABLE `enum_entity` (`id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`as_default` VARCHAR(255) NOT NULL,`as_string` VARCHAR(255) NOT NULL,`as_int` INT NOT NULL);'
}

void "test jpa create table with enums"() {
Expand All @@ -134,7 +134,7 @@ class H2EnumsMappingSpec extends Specification implements H2TestPropertyProvider
def sql = builder.buildBatchCreateTableStatement(PersistentEntity.of(JpaEnumEntity))

then:
sql == 'CREATE TABLE `jpa_enum_entity` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`as_default` INT NOT NULL,`as_string` VARCHAR(255) NOT NULL,`as_int` INT NOT NULL);'
sql == 'CREATE TABLE `jpa_enum_entity` (`id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`as_default` INT NOT NULL,`as_string` VARCHAR(255) NOT NULL,`as_int` INT NOT NULL);'
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@
package io.micronaut.data.model;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.data.annotation.Join;
import io.micronaut.data.model.query.JoinPath;

import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand All @@ -47,21 +45,23 @@ private AssociationUtils() { }
* @return the join paths, if none defined and not of type FETCH then an empty set
*/
public static Set<JoinPath> getJoinFetchPaths(AnnotationMetadata annotationMetadata) {
return annotationMetadata.getAnnotationValuesByType(Join.class).stream().filter(
AssociationUtils::isJoinFetch
).map(av -> {
return getJoinPaths(annotationMetadata).stream()
.filter(jp -> jp.getJoinType().isFetch() || jp.getJoinType() == Join.Type.DEFAULT)
.collect(Collectors.toSet());
}

/**
* Gets all the join paths from the annotation metadata.
* @param annotationMetadata the annotation metadata
* @return the join paths
*/
public static Set<JoinPath> getJoinPaths(AnnotationMetadata annotationMetadata) {
return annotationMetadata.getAnnotationValuesByType(Join.class).stream().map(av -> {
String path = av.stringValue().orElseThrow(() -> new IllegalStateException("Should not include annotations without a value definition"));
Join.Type joinType = av.get("type", Join.Type.class).orElse(Join.Type.DEFAULT);
String alias = av.stringValue("alias").orElse(null);
return new JoinPath(path, new Association[0], joinType, alias);
}).collect(Collectors.toSet());
}

private static boolean isJoinFetch(AnnotationValue<Join> av) {
if (!av.stringValue().isPresent()) {
return false;
}
Optional<String> type = av.stringValue("type");
return !type.isPresent() || type.get().contains("FETCH");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,20 @@ default Map<String, Object> getQueryHints() {

/**
* @return The join paths that require a fetch
* @deprecated Use {@link #getJoinPaths()} and filter the paths
*/
default @NonNull Set<JoinPath> getJoinFetchPaths() {
@Deprecated(forRemoval = true, since = "4.8.1")
@NonNull
default Set<JoinPath> getJoinFetchPaths() {
return Collections.emptySet();
}

/**
* @return The all join paths
* @since 4.8.1
*/
@NonNull
default Set<JoinPath> getJoinPaths() {
return Collections.emptySet();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,9 @@ class Emb {
def employeeGroupSql = builder.buildCreateTableStatements(employeeGroupEntity)
then:"No join table is created"
employeeSql.length == 1
employeeSql[0] == 'CREATE TABLE `employee` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`name` VARCHAR(255) NOT NULL,`category_id` BIGINT NOT NULL,`employer_id` BIGINT NOT NULL);'
employeeSql[0] == 'CREATE TABLE `employee` (`id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`name` VARCHAR(255) NOT NULL,`category_id` BIGINT NOT NULL,`employer_id` BIGINT NOT NULL);'
employeeGroupSql.length == 1
employeeGroupSql[0] == 'CREATE TABLE `employee_group` (`id` BIGINT AUTO_INCREMENT PRIMARY KEY,`name` VARCHAR(255) NOT NULL,`category_id` BIGINT NOT NULL,`employer_id` BIGINT NOT NULL);'
employeeGroupSql[0] == 'CREATE TABLE `employee_group` (`id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`name` VARCHAR(255) NOT NULL,`category_id` BIGINT NOT NULL,`employer_id` BIGINT NOT NULL);'
}

void "test create ManyToMany table with schema"() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ public <T, R> Mono<R> findOne(@NonNull PreparedQuery<T, R> pq) {

SqlTypeMapper<Row, R> mapper = createMapper(preparedQuery, Row.class);
if (mapper instanceof SqlResultEntityTypeMapper<Row, R> entityTypeMapper) {
final boolean hasJoins = !preparedQuery.getJoinFetchPaths().isEmpty();
final boolean hasJoins = !preparedQuery.getJoinPaths().isEmpty();
if (!hasJoins) {
return executeAndMapEachRow(statement, entityTypeMapper::readEntity);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ protected final Object findOne(RepositoryMethodKey methodKey, MethodInvocationCo

protected final Set<JoinPath> getMethodJoinPaths(RepositoryMethodKey methodKey, MethodInvocationContext<T, R> context) {
return methodsJoinPaths.computeIfAbsent(methodKey, repositoryMethodKey ->
AssociationUtils.getJoinFetchPaths(context));
AssociationUtils.getJoinPaths(context));
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,12 +679,12 @@ protected <E, R> SqlTypeMapper<RS, R> createMapper(SqlStoredQuery<E, R> prepared
}
if (isEntityResult || preparedQuery.isDtoProjection()) {
Class<R> resultType = preparedQuery.getResultType();
final Set<JoinPath> joinFetchPaths = preparedQuery.getJoinFetchPaths();
final Set<JoinPath> joinPaths = preparedQuery.getJoinPaths();
if (isEntityResult) {
return new SqlResultEntityTypeMapper<>(
getEntity(resultType),
columnNameResultSetReader,
joinFetchPaths,
joinPaths,
sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
loadListener,
conversionService);
Expand All @@ -710,7 +710,7 @@ protected <E, R> SqlTypeMapper<RS, R> createMapper(SqlStoredQuery<E, R> prepared
return new SqlResultEntityTypeMapper<>(
dtoPersistentEntity,
columnNameResultSetReader,
joinFetchPaths,
joinPaths,
sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
null,
conversionService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public final class DefaultStoredQuery<E, RT> extends DefaultStoredDataOperation<
private final boolean isCount;
private final boolean hasResultConsumer;
private Map<String, Object> queryHints;
private Set<JoinPath> joinPaths = null;
private Set<JoinPath> joinFetchPaths = null;
private final List<QueryParameterBinding> queryParameters;
private final boolean rawQuery;
Expand Down Expand Up @@ -215,12 +216,19 @@ public List<QueryParameterBinding> getQueryBindings() {
@Override
public Set<JoinPath> getJoinFetchPaths() {
if (joinFetchPaths == null) {
Set<JoinPath> set = AssociationUtils.getJoinFetchPaths(method);
this.joinFetchPaths = set.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(set);
this.joinFetchPaths = Collections.unmodifiableSet(AssociationUtils.getJoinFetchPaths(method));
}
return joinFetchPaths;
}

@Override
public Set<JoinPath> getJoinPaths() {
if (joinPaths == null) {
joinPaths = Collections.unmodifiableSet(AssociationUtils.getJoinPaths(method));
}
return joinPaths;
}

/**
* @return The method
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ default Set<JoinPath> getJoinFetchPaths() {
return getStoredQueryDelegate().getJoinFetchPaths();
}

@Override
default Set<JoinPath> getJoinPaths() {
return getStoredQueryDelegate().getJoinPaths();
}

@Override
default boolean isSingleResult() {
return getStoredQueryDelegate().isSingleResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public Set<JoinPath> getJoinFetchPaths() {
return joinPaths;
}

@Override
public Set<JoinPath> getJoinPaths() {
return joinPaths;
}

private static class QueryResultParameterBinding implements QueryParameterBinding {
private final io.micronaut.data.model.query.builder.QueryParameterBinding p;
private final List<QueryParameterBinding> all;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2539,6 +2539,12 @@ abstract class AbstractRepositorySpec extends Specification {
book2.getStudents().add(student)
bookRepository.save(book1)
bookRepository.save(book2)
def otherStudent = new Student("Ioana")
studentRepository.save(otherStudent)
when:
def students = studentRepository.queryByName(otherStudent.name, Pageable.from(0, 2))
then:
students.size() == 1
when:
def loadedStudent = studentRepository.findByName(student.name).get()
def loadedBook1 = bookRepository.findById(book1.id).get()
Expand All @@ -2554,6 +2560,10 @@ abstract class AbstractRepositorySpec extends Specification {
loadedBook2
loadedBook2.title == book2.title
loadedBook2.id == book2.id
when:
students = studentRepository.queryByName(student.name, Pageable.from(0, 2))
then:
students.size() == 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns 2 records but with the same student (because left joined to two books)

cleanup:
studentRepository.delete(student)
bookRepository.delete(book1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.Join;
import io.micronaut.data.annotation.Version;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
import io.micronaut.data.repository.jpa.criteria.QuerySpecification;
Expand All @@ -43,6 +44,9 @@ public interface StudentRepository extends CrudRepository<Student, Long>, JpaSpe
@Join(value = "books", type = Join.Type.FETCH)
Optional<Student> findByName(String name);

@Join(value = "books", type = Join.Type.LEFT)
List<Student> queryByName(String name, Pageable pageable);

static QuerySpecification<Student> byBookTitles(List<String> bookTitles) {
return (root, query, criteriaBuilder) -> {
boolean isCountQuery = query.getResultType().equals(Long.class);
Expand Down
Loading