问题发现
前不久在将Hibernate从4.3.5升级到5.2.17时遇到一个兼容性问题,系统中部分实体类注解方式为:
@Entity
@Table(name = "user", schema = "user_db", catalog = "")
执行数据库查询时,会报如下错误
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'base_db.user' doesn't exist
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
at com.mysql.jdbc.Util.getInstance(Util.java:386)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1052)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4096)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4028)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2490)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2651)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2683)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2144)
at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:2310)
可见sql在生成时,并没有像旧版本一样,生成user_db.user表的访问,而是使用了jdbc链接串中定义的默认数据库base_db.
将数据库名称的定义字段从schema更换为catalog,问题顺利解决。
@Entity
@Table(name = "user", schema = "", catalog = "user_db")
为了弄清楚@Table注解在两个不同版本下如何解析表名,分别查看了两个版本的实现方式。
Hibernate 4.3.5实现方式
首先SingleTableEntityPersister在构造函数中调用table.getQualifiedName获得表解析后的全名。
public SingleTableEntityPersister(
final PersistentClass persistentClass,
final EntityRegionAccessStrategy cacheAccessStrategy,
final NaturalIdRegionAccessStrategy naturalIdRegionAccessStrategy,
final SessionFactoryImplementor factory,
final Mapping mapping) throws HibernateException {
super( persistentClass, cacheAccessStrategy, naturalIdRegionAccessStrategy, factory );
// CLASS + TABLE
joinSpan = persistentClass.getJoinClosureSpan()+1;
qualifiedTableNames = new String[joinSpan];
isInverseTable = new boolean[joinSpan];
isNullableTable = new boolean[joinSpan];
keyColumnNames = new String[joinSpan][];
final Table table = persistentClass.getRootTable();
qualifiedTableNames[0] = table.getQualifiedName(
factory.getDialect(),
factory.getSettings().getDefaultCatalogName(),
factory.getSettings().getDefaultSchemaName()
);
//...省略其他代码
Table类中获取表名使用简单直接的catatlog.schema.name方式。
//Table.class
public String getQualifiedName(Dialect dialect, String defaultCatalog, String defaultSchema) {
if ( subselect != null ) {
return "( " + subselect + " )";
}
String quotedName = getQuotedName( dialect );
String usedSchema = schema == null ?
defaultSchema :
getQuotedSchema( dialect );
String usedCatalog = catalog == null ?
defaultCatalog :
getQuotedCatalog( dialect );
return qualify( usedCatalog, usedSchema, quotedName );
}
public static String qualify(String catalog, String schema, String table) {
StringBuilder qualifiedName = new StringBuilder();
if ( catalog != null ) {
qualifiedName.append( catalog ).append( '.' );
}
if ( schema != null ) {
qualifiedName.append( schema ).append( '.' );
}
return qualifiedName.append( table ).toString();
}
Hibernate 5.2.17实现
AbstractEntityPersister通过determineTableName获得表全名
//AbstractEntityPersister.java
protected String determineTableName(Table table, JdbcEnvironment jdbcEnvironment) {
if ( table.getSubselect() != null ) {
return "( " + table.getSubselect() + " )";
}
return jdbcEnvironment.getQualifiedObjectNameFormatter().format(
table.getQualifiedTableName(),
jdbcEnvironment.getDialect()
);
}
其中JdbcEnvironment是如何决定用哪个QualifiedObjectNameFormatter?
//Hibernate源码中的JdbcEnvironmentImpl
private NameQualifierSupport determineNameQualifierSupport(DatabaseMetaData databaseMetaData) throws SQLException {
final boolean supportsCatalogs = databaseMetaData.supportsCatalogsInTableDefinitions();
final boolean supportsSchemas = databaseMetaData.supportsSchemasInTableDefinitions();
if ( supportsCatalogs && supportsSchemas ) {
return NameQualifierSupport.BOTH;
}
else if ( supportsCatalogs ) {
return NameQualifierSupport.CATALOG;
}
else if ( supportsSchemas ) {
return NameQualifierSupport.SCHEMA;
}
else {
return NameQualifierSupport.NONE;
}
}
//mysql-connector中JDBC4DatabaseMetaData
public boolean supportsSchemasInTableDefinitions() throws SQLException {
return false;
}
可见,由于mysql-connector中已经定义了不支持schema的定义,所以使用的是CATALOG类型的CatalogNameFormat。因此sql中表名称的格式为:catalog.name
private static class CatalogNameFormat implements Format {
private final String catalogSeparator;
public CatalogNameFormat(String catalogSeparator) {
this.catalogSeparator = catalogSeparator;
}
@Override
public String format(Identifier catalog, Identifier schema, Identifier name, Dialect dialect) {
StringBuilder buff = new StringBuilder();
if ( catalog != null ) {
buff.append( render( catalog, dialect ) ).append( catalogSeparator );
}
buff.append( render( name, dialect ) );
return buff.toString();
}
}
小结
- 在Hibernate 4.3.5中,不管用catalog还是schema都可以正确拼接出表名,但是不能两个字段同时使用。
- 在Hibernate 5.2.17中,schema属性可以完全忽略。
- 在mysql中schema和database可以理解为相同的概念,所以mysql-connector实现也不再支持schema了。