Потратил на эту проблему несколько часов. Чисто из-за собственной глупости. Сидел и отлаживал с точками остановки код Hibernate, чтобы понять, что происходит, но в итоге всё же нашёл на источник проблемы, хотя как мне кажется, он был вполне очевидным.
Проблема была в отображении связи М:М:
1 2 3 4 |
@ManyToMany @JoinTable( inverseJoinColumns = @JoinColumn(name="book_id")) private Set<Book> books; |
В этом куске кода происходит отображение связи трёх таблиц:
В таблицах book и pet показаны только первичные ключи, чтобы не загромождать рисунок лишней информацией.
Проблема с кодом выше заключалась в том, что отображение на таблицы идеально отрабатывало в примере для Spring Framework, но при попытке выборки записей для коллекции books падало с ошибкой в примере для Spring Boot:
1 2 |
org.hibernate.exception.SQLGrammarException: JDBC exception executing SQL [select b1_0."pet_id",b1_1."id",b1_1."bookcase_id",b1_1."bookcase_order",b1_1."hidden_objects_game_drop_rate" from "virtualpets_server_springboot"."pet_books" b1_0 join "virtualpets_server_springboot"."book" b1_1 on b1_1."id"=b1_0."book_id" where b1_0."pet_id"=?] [ERROR: relation "virtualpets_server_springboot.pet_books" does not exist Позиция: 115] |
Как видно из текста ошибки, Hibernate для Spring Boot почему-то отображает @ManyToMany через промежуточную таблицу pet_books, что довольно странно.
В JavaDoc для аннотации @JoinTable написано, что если атрибут аннотации name не заполнен, то имя таблицы генерируется как имена таблиц двух связанных сущностей, разделённые символом подчёркивания. Откуда взялось множественное число books для варианта со Spring Boot?
Можно конечно, вручную выставить имя таблицы в атрибуте name аннотации @JoinTable, но мне нужен был пример с отсутствующим атрибутом name для книги.
Сейчас я прекрасно понимаю, вполне было очевидно, что проблема в стратегии именования. В логической стратегии именования или физической. Но тогда я что-то затупил. Однако в процессе отладки нашёл класс, который генерирует pet_books:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/* * Copyright 2012-2019 the original author or 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 org.springframework.boot.orm.jpa.hibernate; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.naming.ImplicitJoinTableNameSource; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl; /** * Hibernate {@link ImplicitNamingStrategy} that follows Spring recommended naming * conventions. Naming conventions implemented here are identical to * {@link ImplicitNamingStrategyJpaCompliantImpl} with the exception that join table names * are of the form * <code>{owning_physical_table_name}_{association_owning_property_name}</code>. * * @author Andy Wilkinson * @since 1.4.0 */ public class SpringImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl { @Override public Identifier determineJoinTableName(ImplicitJoinTableNameSource source) { String name = source.getOwningPhysicalTableName() + "_" + source.getAssociationOwningAttributePath().getProperty(); return toIdentifier(name, source.getBuildingContext()); } } |
Класс SpringImplicitNamingStrategy наследуется от ImplicitNamingStrategyJpaCompliantImpl и переопределяет метод determineJoinTableName, генерируя имя таблицы как конкатенацию имени таблицы сущности, владеющей связью, и имени поля. А имя поля в нашем случае как раз books, вот и получается pet_books.
Сложно сказать, для чего в Spring Boot ввели свою стратегию именования, отличающуюся от Jakarta Persistence API, но исправить очень легко, нужно просто в файле “application.properties” выставить implicit_naming_strategy = default:
1 |
spring.jpa.properties.hibernate.implicit_naming_strategy=default |
Значение default — это сокращённая запись для org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl.
Join, join, join your table,
Annotate the scheme,
Merrily merrily, merrily, merrily
Life is but a dream