Skip to content

Commit

Permalink
Fix: Find one with join issue
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov committed Sep 19, 2024
1 parent 792e8a0 commit 588ab32
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.micronaut.data.jdbc.h2.joinissue;

import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.Relation;

import java.util.Set;

@MappedEntity("ji_author")
public record Author(
@Id
@GeneratedValue
Long id,
String name,
@Relation(value = Relation.Kind.ONE_TO_MANY, cascade = Relation.Cascade.ALL, mappedBy = "author")
Set<Book> books) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.micronaut.data.jdbc.h2.joinissue;

import io.micronaut.data.annotation.Join;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

import java.util.List;
import java.util.Optional;

@JdbcRepository(dialect = Dialect.H2)
@Join(value = "books", type = Join.Type.LEFT_FETCH)
public interface AuthorRepository extends CrudRepository<Author, Long> {

Optional<Author> queryByName(String name);

List<Author> queryByNameContains(String partialName);

Optional<Author> findByNameContains(String partialName); //findByNameContainsIgnoreCase has the same issue

//Note: findFirstByNameContains returns only the first row and therefore only one book.

/*
SELECT author_.`id`,
author_.`name`,
author_books_.`id` AS books_id,
author_books_.`title` AS books_title,
author_books_.`author` AS books_author
FROM (
SELECT id,name FROM author
WHERE (`name` LIKE CONCAT('%',:partialName,'%'))
LIMIT 1) author_
LEFT JOIN book author_books_ ON author_.id=author_books_.author;
*/
@Query("SELECT author_.`id`,author_.`name`,author_books_.`id` AS books_id,author_books_.`title` AS books_title,author_books_.`author` AS books_author FROM (SELECT id,name FROM ji_author WHERE (`name` LIKE CONCAT('%',:partialName,'%')) LIMIT 1) author_ LEFT JOIN ji_book author_books_ ON author_.id=author_books_.author;")
Optional<Author> getOneByNameContains(String partialName);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.micronaut.data.jdbc.h2.joinissue

import io.micronaut.data.jdbc.h2.H2DBProperties
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@MicronautTest
@H2DBProperties
class AuthorTest extends Specification {

@Inject
AuthorRepository authorRepository

void test() {
given:
var authorList = List.of(
new Author(null, "Joe Doe",
Set.of(new Book(null, "History of nothing"))),
new Author(null, "Jane Doe",
Set.of(new Book(null, "History of everything"),
new Book(null, "Doing awesome things"))))

authorRepository.saveAll(authorList)

when:
Author author = authorRepository.queryByName("Joe Doe").orElse(null)
then:
author.name() == "Joe Doe"
author.books().size() == 1

when:
List<Author> list = authorRepository.queryByNameContains("Doe")
then:
list.size() == 2
list.get(0).name() == "Joe Doe"
list.get(0).books().size() == 1
list.get(1).name() == "Jane Doe"
list.get(1).books().size() == 2

when:
author = authorRepository.getOneByNameContains("Doe").orElse(null)
then:
author.name() == "Joe Doe"
author.books().size() == 1

when:
author = authorRepository.getOneByNameContains("ne Doe").orElse(null)
then:
author.name() == "Jane Doe"
author.books().size() == 2

when:
author = this.authorRepository.findByNameContains("Doe").orElse(null)
then:
author.name() == "Joe Doe"
author.books().size() == 1

when:
author = this.authorRepository.findByNameContains("e Doe").orElse(null)
then:
author.name() == "Joe Doe"
author.books().size() == 1

when:
author = this.authorRepository.findByNameContains("ne Doe").orElse(null)
then:
author.name() == "Jane Doe"
author.books().size() == 2
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.micronaut.data.jdbc.h2.joinissue;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.MappedProperty;
import io.micronaut.data.annotation.Relation;

@MappedEntity("ji_book")
public record Book(
@Id
@GeneratedValue
Long id,
String title,
@Nullable
@Relation(value = Relation.Kind.MANY_TO_ONE)
@MappedProperty("author")
Author author) {
public Book(Long id, String title) {
this(id, title, null);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.micronaut.data.jdbc.h2.joinissue;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.Relation;

import java.util.Set;

@MappedEntity("ji_director")
public class Director {

@Id
@GeneratedValue
private Long id;

private String name;

@Relation(value = Relation.Kind.ONE_TO_MANY, cascade = Relation.Cascade.ALL, mappedBy = "director")
Set<Movie> movies;

public Director(String name, @Nullable Set<Movie> movies) {
this.name = name;
this.movies = movies;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Set<Movie> getMovies() {
return movies;
}

public void setMovies(Set<Movie> movies) {
this.movies = movies;
}

}

// @Override
// public String toString() {
// return "Director{" +
// "id=" + id +
// ", name='" + name + '\'' +
// ", movies=" + movies +
// '}';
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.micronaut.data.jdbc.h2.joinissue;

import io.micronaut.data.annotation.Join;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

import java.util.List;
import java.util.Optional;

@JdbcRepository(dialect = Dialect.H2)
public interface DirectorRepository extends CrudRepository<Director, Long> {

@Join(value = "movies", type= Join.Type.LEFT_FETCH)
Optional<Director> queryByName(String name);

@Join(value = "movies", type = Join.Type.LEFT_FETCH)
Optional<Director> findByNameContains(String partialName);

@Join(value = "movies", type = Join.Type.LEFT_FETCH)
List<Director> queryByNameContains(String partialName);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.micronaut.data.jdbc.h2.joinissue

import io.micronaut.data.jdbc.h2.H2DBProperties
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@MicronautTest
@H2DBProperties
class DirectorSpec extends Specification {

@Inject
DirectorRepository directorRepository

def 'test'() {
given:
var directorList = List.of(
new Director("John Jones",
Set.of(new Movie("Random Movie"))),
new Director("Ann Jones",
Set.of(new Movie("Super Hero Movie"),
new Movie("Anther Movie with Heroes"))))

directorRepository.saveAll(directorList)

when:
var director = directorRepository.queryByName("John Jones").orElse(null)
then:
director.getName() == "John Jones"
director.getMovies().size() == 1

when:
var list = directorRepository.queryByNameContains("n Jones")
then:
list.size() == 2
list.get(0).getName() == "John Jones"
list.get(0).getMovies().size() == 1
list.get(1).getName() == "Ann Jones"
list.get(1).getMovies().size() == 2

when:
director = directorRepository.findByNameContains("n Jones").orElse(null)
then:
director.getName() == "John Jones"
director.getMovies().size() == 1

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.micronaut.data.jdbc.h2.joinissue;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.MappedProperty;
import io.micronaut.data.annotation.Relation;

@MappedEntity("ji_movie")
public class Movie {

@Id
@GeneratedValue
private Long id;

private String title;

@Nullable
@Relation(Relation.Kind.MANY_TO_ONE)
@MappedProperty("director")
private Director director;

public Movie(String title) {
this.title = title;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Director getDirector() {
return director;
}

public void setDirector(Director director) {
this.director = director;
}

@Override
public String toString() {
return "Movie{" +
"id=" + id +
", title='" + title + '\'' +
", director=" + director +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,15 +263,23 @@ public PushingMapper<RS, R> readOneMapper() {
return new PushingMapper<>() {

final MappingContext<R> ctx = MappingContext.of(entity, startingPrefix);
Object entityId;
R entityInstance;

@Override
public void processRow(RS row) {
if (entityInstance == null) {
Object id = readEntityId(row, ctx);
if (id == null) {
throw new IllegalStateException("Entity needs to have an ID when JOINs are used!");
}
if (entityId == null) {
entityId = id;
entityInstance = readEntity(row, ctx, null, null);
} else {
readChildren(row, entityInstance, null, ctx);
} else if (entityId != id) {
// We want only one entity, every thing else should be skipped
return;
}
readChildren(row, entityInstance, null, ctx);
}

@Override
Expand Down

0 comments on commit 588ab32

Please sign in to comment.