Добавить объект с InheritanceType.JOINED в собственный запрос

Я пытаюсь заставить родные запросы работать с InheritanceType.JOINED. Из документации Hibernate я нашел:

13.1.6. Обработка наследования

Собственные SQL-запросы, которые запрашивают объекты, которые отображаются как часть наследования, должны включать все свойства для базового класса и всех его подклассов.

Использование следующих двух объектов:

@Data
@Entity
@Table(name = "my_super")
@EqualsAndHashCode(of = {"id"})
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class MySuper {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

}

и

@Data
@Entity
@Table(name = "my_sub_a")
@EqualsAndHashCode(callSuper = true)
public class MySubA extends MySuper {

    @Column(name = "x")
    private int x;

}

Когда я пытаюсь создать собственный запрос, используя:

Object actual = session
    .createNativeQuery("SELECT {s.*} FROM my_super {s} LEFT JOIN my_sub_a {a} USING (id)")
    .addEntity("s", MySuper.class)
    .getSingleResult();

Это переводит на запрос:

SELECT s.id as id1_1_0_, s_1_.x as x1_0_0_, case when s_1_.id is not null then 1 when s.id is not null then 0 end as clazz_0_ FROM my_super s LEFT JOIN my_sub_a  a  USING (id) 

И затем сбой:

javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: could not prepare statement
Caused by: org.hibernate.exception.SQLGrammarException: could not prepare statement
Caused by: org.h2.jdbc.JdbcSQLException: Column "S_1_.X" not found; SQL statement:

Итак, мы наблюдаем, что инъекция псевдонима делает свою работу, выясняя, что ей может понадобиться столбец x my_sub_a. Однако он не может найти псевдоним для my_sub_a. Как мой код должен быть изменен так, чтобы этот псевдоним был правильно подключен?

Мой код доступен в https://gist.github.com/JWGmeligMeyling/51e8a305f3c268eda473511e202f76e8 для легкого воспроизведения моей проблемы.

(Я знаю, что этот запрос может быть легко выражен и в JPQL или HQL, и даже может быть достигнут с помощью API EntityManager и Session. Однако я хочу использовать это в более сложном запросе, что я упростил все детали, необходимые для этого вопроса).

Ответ 1

Проблема, похоже, связана с используемым диалектом базы данных, другими словами, с некоторыми "экзотическими" частями запроса. Поведение, которое вы описали, можно тиражировать с предоставленным вами запросом, но с небольшими тиражами, которые он запускает без ошибок - в зависимости от диалекта, который вы хотите использовать.

В вашем примере вы используете базу данных H2, но я предполагаю, что это не ваш производственный диалект, не так ли? Я также попробовал его с базой данных PostgresSQL (в версии 9.5).

С исходным запросом поведение одинаково для H2 и PostgreSQL. Но если вы удалите фигурные скобки из ваших столбцов и псевдонимов (которые кажутся как escape-последовательности ODBC) и измените USING clause в явное условие ON a.id = s.id, запрос выполним без каких-либо исключений.

Чтобы проверить поведение, я создал несколько тестов с разными запросами, используя сеанс Hibernate или EntityManager, потому что, взглянув на ваш код связанного примера, я каким-то образом смутился запутанным использованием Hibernate Session interface и Методы EntityManager, такие как createNativeQuery. В случае сомнений я попробовал оба.  Я использовал те же объекты и более или менее тот же конфигурационный и тестовый код, что и в вашем примере, но в Spring Загрузочная среда, только для удобства. Для переключения между базами данных я использовал Spring Boot Profiles, просто активируйте/раскомментируйте @ActiveProfiles("postgres") если у вас есть конфигурации для обеих баз данных.

Вот тесты, я надеюсь, что это поможет немного:

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.hibernate.Session;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
//@ActiveProfiles("postgres") // activate for PostgreSQL tests
public class InheritanceDemoApplicationTests {

    private static final String QUERY = "SELECT {s.*} FROM my_super {s} LEFT JOIN my_sub_a {a} USING (id)";
    private static final String QUERY_WITHOUT_ODBC_ESCAPES = "SELECT s.* FROM my_super s LEFT JOIN my_sub_a a USING (id)";
    private static final String QUERY_WITHOUT_USING_KEYWORD = "SELECT {s.*} FROM my_super {s} LEFT JOIN my_sub_a {a} ON a.id = s.id";
    private static final String QUERY_WITHOUT_ODBC_ESCAPES_AND_WITHOUT_USING_KEYWORD = "SELECT s.* FROM my_super s LEFT JOIN my_sub_a a ON a.id = s.id";

    @PersistenceContext
    private EntityManager entityManager;

    @Test
    public void sessionQuery() {
        validateQueryViaSession(QUERY);
    }

    @Test
    public void entityManagerQuery() {
        validateQueryViaEntityManager(QUERY);
    }

    @Test // works for PostgreSQL
    public void sessionQueryWithoutOdbc() {
        validateQueryViaSession(QUERY_WITHOUT_ODBC_ESCAPES);
    }

    @Test // works for PostgreSQL
    public void entityManagerQueryWithoutOdbc() {
        validateQueryViaEntityManager(QUERY_WITHOUT_ODBC_ESCAPES);
    }

    @Test
    public void sessionQueryWithoutUsing() {
        validateQueryViaSession(QUERY_WITHOUT_USING_KEYWORD);
    }

    @Test // works for H2
    public void entityManagerQueryWithoutUsing() {
        validateQueryViaEntityManager(QUERY_WITHOUT_USING_KEYWORD);
    }

    @Test // works for H2 & PostgreSQL
    public void sessionQueryWithoutOdbcAndWithoutUsing() {
        validateQueryViaSession(QUERY_WITHOUT_ODBC_ESCAPES_AND_WITHOUT_USING_KEYWORD);
    }

    @Test // works for H2 & PostgreSQL
    public void entityManagerQueryWithoutOdbcAndWithoutUsing() {
        validateQueryViaEntityManager(QUERY_WITHOUT_ODBC_ESCAPES_AND_WITHOUT_USING_KEYWORD);
    }

    @SuppressWarnings("rawtypes")
    private void validateQueryViaSession(final String queryString) {
        final MySubA match = persistMySubA();
        List result = entityManager.unwrap(Session.class).createSQLQuery(queryString).addEntity("s", MySuper.class)
                .list();
        assertThat(result.iterator().next()).isEqualToComparingFieldByField(match);
    }

    @SuppressWarnings("rawtypes")
    private void validateQueryViaEntityManager(final String queryString) {
        final MySubA match = persistMySubA();
        List result = entityManager.createNativeQuery(queryString, MySuper.class).getResultList();
        assertThat(result.iterator().next()).isEqualToComparingFieldByField(match);
    }

    private MySubA persistMySubA() {
        final MySubA mySubA = new MySubA();
        mySubA.setX(1);
        entityManager.persist(mySubA);
        entityManager.flush();
        return mySubA;
    }

}