diff --git a/documentation/src/main/asciidoc/querylanguage/From.adoc b/documentation/src/main/asciidoc/querylanguage/From.adoc index 6e1674026e5e..f77dbd8de0b9 100644 --- a/documentation/src/main/asciidoc/querylanguage/From.adoc +++ b/documentation/src/main/asciidoc/querylanguage/From.adoc @@ -488,7 +488,7 @@ where pub.name like :pubName [[collection-valued-associations]] ==== Joining collections and many-valued associations -When a join involves a collection or many-valued association, the declared identification variable refers to the _elements_ of the collection, that is: +When a join involves a collection or many-valued association, by default, the declared identification variable refers to the _elements_ of the collection, that is: - to the elements of a `Set`, - to the elements of a `List`, not to their indices in the list, or @@ -507,6 +507,17 @@ where author.name like :namePattern In this example, the identification variable `author` is of type `Author`, the element type of the list `Book.authors`. But if we need to refer to the index of an `Author` in the list, we need some extra syntax. +If the key of a map is an entity, we may join it using the `key()` function: + +[source, hql] +---- +select book.isbn, lang.language, lang.country, lang.variant +from Book book +join key(book.translations) as lang +---- + +It's also legal, but redundant, to use the `value()` function when joining the value of a map. + You might recall that we mentioned <> and <> a bit earlier. These functions may be applied to the identification variable declared in a collection join or many-valued association join. @@ -544,6 +555,13 @@ where leadAuthor.name like :namePattern and index(leadAuthor) == 0 ---- +[source, hql] +---- +select book.isbn, key(translation).language, key(translation).country +from Book book +join book.translations as translation +---- + [[implicit-collection-join]] ==== Implicit joins involving collections diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/MapJoinKayValueTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/MapJoinKayValueTests.java new file mode 100644 index 000000000000..b5827be32251 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/MapJoinKayValueTests.java @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Jpa(annotatedClasses = {MapJoinKayValueTests.Book.class, + MapJoinKayValueTests.Language.class}) +class MapJoinKayValueTests { + @Test void test(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + scope.inTransaction( s -> { + Language language = new Language(); + language.id = "en_AU"; + language.language = "en"; + language.country = "AU"; + s.persist( language ); + Book book = new Book(); + book.isbn = "978-1932394153"; + book.title = "Hibernate in Action"; + book.translations = Map.of( language, book ); + s.persist( book ); + var t = + s.createQuery( "select t from Book b join b.translations t", Book.class ) + .getSingleResult(); + assertEquals(book, t); + var title = + s.createQuery( "select t.title from Book b join b.translations t", String.class ) + .getSingleResult(); + assertEquals("Hibernate in Action", title); + } ); + } + @Test void testValue(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + scope.inTransaction( s -> { + Language language = new Language(); + language.id = "en_AU"; + language.language = "en"; + language.country = "AU"; + s.persist( language ); + Book book = new Book(); + book.isbn = "978-1932394153"; + book.title = "Hibernate in Action"; + book.translations = Map.of( language, book ); + s.persist( book ); + var b = + s.createQuery( "select t from Book b join value(b.translations) t", Book.class ) + .getSingleResult(); + assertEquals(book, b); + var title = + s.createQuery( "select t.title from Book b join value(b.translations) t", String.class ) + .getSingleResult(); + assertEquals("Hibernate in Action", title); + } ); + } + @Test void testKey(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + scope.inTransaction( s -> { + Language language = new Language(); + language.id = "en_AU"; + language.language = "en"; + language.country = "AU"; + s.persist( language ); + Book book = new Book(); + book.isbn = "978-1932394153"; + book.title = "Hibernate in Action"; + book.translations = Map.of( language, book ); + s.persist( book ); + var l = + s.createQuery( "select l from Book b join key(b.translations) l", Language.class ) + .getSingleResult(); + assertEquals(language, l); + var c1 = + s.createQuery( "select l.country from Book b join key(b.translations) l", String.class ) + .getSingleResult(); + assertEquals("AU", c1); + var c2 = + s.createQuery( "select key(t).country from Book b join b.translations t", String.class ) + .getSingleResult(); + assertEquals("AU", c2); + } ); + } + + @Entity(name="Book") + static class Book { + @Id String isbn; + String title; + @ManyToMany + Map translations; + } + + @Entity + static class Language { + @Id + String id; + String language; + String country; + String variant; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/MapJoinTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/MapJoinTests.java new file mode 100644 index 000000000000..0d7583c157a9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/MapJoinTests.java @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Jpa(annotatedClasses = MapJoinTests.Book.class) +class MapJoinTests { + @Test void test(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + scope.inTransaction( s -> { + Book book = new Book(); + book.isbn = "978-1932394153"; + book.title = "Hibernate in Action"; + book.translations = Map.of( "en", book ); + s.persist( book ); + var t = + s.createQuery( "select t from Book b join b.translations t", Book.class ) + .getSingleResult(); + assertEquals(book, t); + var title = + s.createQuery( "select t.title from Book b join b.translations t", String.class ) + .getSingleResult(); + assertEquals("Hibernate in Action", title); + } ); + } + @Test void testValue(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + scope.inTransaction( s -> { + Book book = new Book(); + book.isbn = "978-1932394153"; + book.title = "Hibernate in Action"; + book.translations = Map.of( "en", book ); + s.persist( book ); + var b = + s.createQuery( "select t from Book b join value(b.translations) t", Book.class ) + .getSingleResult(); + assertEquals(book, b); + var title = + s.createQuery( "select t.title from Book b join value(b.translations) t", String.class ) + .getSingleResult(); + assertEquals("Hibernate in Action", title); + } ); + } + @FailureExpected(jiraKey = "HHH-19759") + @Test void testKey(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().getSchemaManager().truncate(); + scope.inTransaction( s -> { + Book book = new Book(); + book.isbn = "978-1932394153"; + book.title = "Hibernate in Action"; + book.translations = Map.of( "en", book ); + s.persist( book ); + var lang = + s.createQuery( "select l from Book b join key(b.translations) l", String.class ) + .getSingleResult(); + assertEquals("en", lang); + } ); + } + + @Entity(name="Book") + static class Book { + @Id String isbn; + String title; + @ManyToMany + Map translations; + } +}