Spring Boot首次启动后重启失败:外键元数据解析异常
问题背景
开发Spring Boot REST CRUD服务,包含UserMetadata和Organization两个实体,UserMetadata通过多对一关联引用Organization的外键。首次启动服务生成数据库表后,重启应用会抛出如下错误:
org.hibernate.tool.schema.spi.SchemaManagementException: Attempt to resolve foreign key metadata from JDBC metadata failed to find column mappings for foreign key named [FK_OrganizationToMetadata]
删除数据库表后可正常启动,但无法每次重启都删除表。移除org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl物理命名策略可解决问题,但希望保留该策略。
实体代码
Organization实体
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @Data @Entity @Table(name = "Organizations-v2") public class Organization { // This UUID should come from the matching Keycloak Group @Id @Column(name="id", nullable=false) UUID id; @Column(nullable = false, columnDefinition = "text") String name; @Column(columnDefinition = "text") String address1; @Column(columnDefinition = "text") String address2; @Column(columnDefinition = "text") String stateProvince; @Column(columnDefinition = "text") String country; @Column(columnDefinition = "text") String city; Integer postalCode; @Column(columnDefinition = "text") String availableSoftwareProducts; }
UserMetadata实体
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @Data @Entity @Table(name = "UserMetadata") public class UserMetadata { @Id @Column(name = "user-id", nullable = false) UUID userID; @ManyToOne @JoinColumn(name = "org-id", referencedColumnName = "id", foreignKey=@ForeignKey(name = "FK_OrganizationToMetadata")) Organization organization; @Column(name = "user-product-args", columnDefinition = "text") String userProductArguments; @Column(name = "is-org-admin", nullable = false) boolean isOrgAdmin; public Map<String, String> getParsedUserProductArguments() { if(userProductArguments == null) return Map.of(); Map<String, String> map = new HashMap<>(); for(String str : userProductArguments.split(",")) { String[] keyValue = str.split("="); if(keyValue.length == 2) map.put(keyValue[0], keyValue[1]); } return map; } }
数据库表结构
- Organizations-v2表:主键为
id(UUID类型),包含name、address1等文本类型字段 - UserMetadata表:主键为
user-id(UUID类型),org-id为外键关联Organizations-v2的id列,外键名称为FK_OrganizationToMetadata
完整错误栈
2024-06-28T11:34:40.229-06:00 ERROR 23472 --- [cORE] [ main] o.s.boot.SpringApplication : Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Attempt to resolve foreign key metadata from JDBC metadata failed to find column mappings for foreign key named [FK_OrganizationToMetadata] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:952) ~[spring-context-6.1.10.jar:6.1.10] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.10.jar:6.1.10] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.1.jar:3.3.1] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.1.jar:3.3.1] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.1.jar:3.3.1] at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.1.jar:3.3.1] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.1.jar:3.3.1] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.1.jar:3.3.1] at design.ore.cORE.Main.main(Main.java:37) ~[main/:na] Caused by: jakarta.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Attempt to resolve foreign key metadata from JDBC metadata failed to find column mappings for foreign key named [FK_OrganizationToMetadata] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:421) ~[spring-orm-6.1.10.jar:6.1.10] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-6.1.10.jar:6.1.10] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366) ~[spring-orm-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1835) ~[spring-beans-6.1.10.jar:6.1.10] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1784) ~[spring-beans-6.1.10.jar:6.1.10] ... 15 common frames omitted Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Attempt to resolve foreign key metadata from JDBC metadata failed to find column mappings for foreign key named [FK_OrganizationToMetadata] at org.hibernate.tool.schema.extract.internal.AbstractInformationExtractorImpl$ForeignKeyBuilderImpl.build(AbstractInformationExtractorImpl.java:1383) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.extract.internal.AbstractInformationExtractorImpl$ForeignKeyBuilderImpl.build(AbstractInformationExtractorImpl.java:1364) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.extract.internal.AbstractInformationExtractorImpl.getForeignKeys(AbstractInformationExtractorImpl.java:1348) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.extract.internal.TableInformationImpl.foreignKeys(TableInformationImpl.java:97) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.extract.internal.TableInformationImpl.getForeignKeys(TableInformationImpl.java:91) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.equivalentForeignKeyExistsInDatabase(AbstractSchemaMigrator.java:484) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.checkForExistingForeignKey(AbstractSchemaMigrator.java:476) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.applyForeignKeys(AbstractSchemaMigrator.java:443) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.performMigration(AbstractSchemaMigrator.java:268) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.doMigration(AbstractSchemaMigrator.java:117) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:280) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.lambda$process$5(SchemaManagementToolCoordinator.java:144) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at java.base/java.util.HashMap.forEach(HashMap.java:1429) ~[na:na] at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:141) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.boot.internal.SessionFactoryObserverForSchemaExport.sessionFactoryCreated(SessionFactoryObserverForSchemaExport.java:37) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.internal.SessionFactoryObserverChain.sessionFactoryCreated(SessionFactoryObserverChain.java:35) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:322) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:457) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1506) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75) ~[spring-orm-6.1.10.jar:6.1.10] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390) ~[spring-orm-6.1.10.jar:6.1.10] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-6.1.10.jar:6.1.10] ... 19 common frames omitted
解决方案
原因分析
PhysicalNamingStrategyStandardImpl会严格使用实体中定义的名称,但Organizations-v2表名包含减号这种特殊字符,Hibernate在解析JDBC元数据时,数据库可能需要用引号包裹特殊字符的表名才能正确识别,导致Hibernate无法匹配外键关联的表和列映射。
方案1:自定义物理命名策略,为特殊字符表名添加引号
继承PhysicalNamingStrategyStandardImpl,重写表名转换方法,对包含特殊字符的表名添加对应数据库的引号(比如MySQL用反引号,PostgreSQL用双引号):
package com.yourpackage; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; public class QuotedPhysicalNamingStrategy extends PhysicalNamingStrategyStandardImpl { @Override public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { Identifier original = super.toPhysicalTableName(name, context); // 检测表名是否包含特殊字符,这里以减号为例 if (original.getText().contains("-")) { // 根据数据库类型选择引号,MySQL用`,PostgreSQL用" return Identifier.toIdentifier("`" + original.getText() + "`", original.isQuoted()); } return original; } }
在配置文件中替换物理命名策略:
# application.properties spring.jpa.hibernate.naming.physical-strategy=com.yourpackage.QuotedPhysicalNamingStrategy
方案2:修改表名,移除特殊字符
将Organizations-v2改为Organizations_v2或者OrganizationsV2,避免使用减号这类特殊字符,这样Hibernate无需额外处理即可正常解析元数据:
@Table(name = "Organizations_v2") public class Organization { // ... 其余代码不变 }
方案3:验证外键定义一致性
确认@JoinColumn的referencedColumnName与Organization实体的主键列名完全一致,同时检查数据库中外键FK_OrganizationToMetadata确实正确关联了UserMetadata.org-id和Organizations-v2.id。
内容的提问来源于stack exchange,提问作者Jacob Batista

