Using @Table with schema name in Hibernate 3.3.1ga

2019-03-29 04:34发布

问题:

How can I make this work in unit tests using Hibernate 3.3.1ga and HSQLDB:

@Entity
@Table(name="CATEGORY", schema="TEST")
public static class Category { ... }

The problem is that Hibernate expects the schema to exist. The second problem is that Hibernate issues the CREATE TABLE TEST.CATEGORY before any of my code runs (this happens deep inside Spring's test setup), so I can't get a connection to the DB before Hibernate and create the schema manually.

But I need the schema because I have to access different databases in the real code. What should I do?

Hibernate 3.3.1ga, HSQLDB, Spring 2.5

回答1:

You could write a class that implements InitializingBean:

public class SchemaCreator implements InitializingBean {

    private String schema;
    private DataSource dataSource;

    public String getSchema() {
        return schema;
    }

    public void setSchema(String schema) {
        this.schema = schema;
    }

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.execute("CREATE SCHEMA " + schema + " AUTHORIZATION DBA");
    }

}

You then have to define a bean in your bean definition file of this class (I'm taking a shot in the dark as to what your existing bean definitions look like).

<bean id="dataSource" class="...">
    <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
    <property name="url" value="jdbc:hsqldb:mem:test"/>
    <property name="username" value="sa"/>
    <property name="password" value=""/>
</bean>

<bean id="sessionFactory" depends-on="schemaCreator" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    ...
</bean>

<bean id="schemaCreator" class="SchemaCreator">
    <property name="dataSource" ref="dataSource"/>
    <property name="schema" value="TEST"/>
</bean>

By using the depends-on attribute of Hibernate's bean, Spring will ensure that the schemaCreator bean will be initialized first, causing the schema to exist just in time. This should also make your intentions clearer.



回答2:

My current solution looks like this:

@Override
protected String[] getConfigLocations() {
    createHSQLDBSchemas ();

    return new String[]{
            "test-spring-config.xml"
    };
}

private static boolean hsqldbSchemasCreated = false;

public static void createHSQLDBSchemas ()
{
    if (hsqldbSchemasCreated)
        return;

    try
    {
        log.info ("createHSQLDBSchemas");

        Class.forName("org.hsqldb.jdbcDriver").newInstance();
        Connection c = DriverManager.getConnection("jdbc:hsqldb:mem:test", "sa", "");
        Statement stmt = c.createStatement ();

        String sql;
        sql = "CREATE SCHEMA xxx AUTHORIZATION DBA";
        log.info (sql);
        stmt.execute (sql);

        stmt.close ();
        c.close ();
    }
    catch (Exception e)
    {
        throw new ShouldNotHappenException (e);
    }

    hsqldbSchemasCreated = true;
}

but that feels like a really ugly hack. Isn't there a better solution?



回答3:

Below is an example of how you can create spring config with test hslqdb It automaticly detects all your schemas from @Table(schema =...) and creates them for you.

If it is just for testing this should work for you:

import org.reflections.Reflections; //maven artifact: 'org.reflections:reflections:0.9.9-RC1'
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;

import javax.persistence.Table;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

@Configuration
@ComponentScan("com.test.collection")
public class CollectionConfig {

private static final String[] ENTITY_PACKAGES = { "com.test.collection.domain.dao" };
private static final String CONFIGURATION_LOCATION = "/movie-collection-hibernate.cfg.xml";

@Bean( name = "testSessionFactory" )
@Lazy
public LocalSessionFactoryBean getTestSessionFactory() {
    LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
    sessionFactory.setPackagesToScan( ENTITY_PACKAGES );

    Properties hibernateProperties = getHibernateHsqlTestDbProperties();
    sessionFactory.setHibernateProperties( hibernateProperties );

    createNonStandardSchemas( hibernateProperties );

    return sessionFactory;
}

private void createNonStandardSchemas( Properties properties ) {
    final String DEFAULT_SCHEMA = "";

    Set<String> schemas = new HashSet<>();
    Reflections reflections = new Reflections( ENTITY_PACKAGES );
    Set<Class<?>> annotatedClasses =
            reflections.getTypesAnnotatedWith( Table.class );

    for ( Class<?> clazz : annotatedClasses ) {
        Table table = clazz.getAnnotation( Table.class );
        if ( !DEFAULT_SCHEMA.equals( table.schema() ) ) {
            schemas.add( table.schema() );
        }
    }

    if ( !schemas.isEmpty() ) {
        DriverManagerDataSource driverManager = new DriverManagerDataSource();
        driverManager.setDriverClassName( properties.getProperty( "hibernate.connection.driver_class" ) );
        driverManager.setUrl( properties.getProperty( "hibernate.connection.url" ) );
        driverManager.setUsername( properties.getProperty( "hibernate.connection.username" ) );
        driverManager.setPassword( properties.getProperty( "hibernate.connection.password" ) );

        JdbcTemplate jdbcTemplate = new JdbcTemplate( driverManager );

        for ( String schemaName : schemas ) {
            jdbcTemplate.execute(
                    String.format( "DROP SCHEMA IF EXISTS %s", schemaName)
            );
            jdbcTemplate.execute(
                    String.format( "CREATE SCHEMA %s AUTHORIZATION DBA", schemaName)
            );
        }
    }
}


private Properties getHibernateHsqlTestDbProperties() {
    Properties prop = new Properties();
    prop.setProperty( "hibernate.connection.driver_class", "org.hsqldb.jdbcDriver" );
    prop.setProperty( "hibernate.connection.url", "jdbc:hsqldb:mem:test" );
    prop.setProperty( "hibernate.connection.username", "sa" );
    prop.setProperty( "hibernate.connection.password", "test" );
    prop.setProperty( "hibernate.connection.pool_size", "5" );
    prop.setProperty( "hibernate.dialect", "org.hibernate.dialect.HSQLDialect" );
    prop.setProperty( "hibernate.current_session_context_class", "thread" );
    prop.setProperty( "hibernate.cache.provider_class", "org.hibernate.cache.internal.NoCachingRegionFactory" );
    prop.setProperty( "hibernate.show_sql", "false" );
    prop.setProperty( "hibernate.format_sql", "false" );
    prop.setProperty( "hibernate.use_sql_comments", "false" );
    prop.setProperty( "hibernate.hbm2ddl.auto", "create-drop" );
    return prop;
}


}

And here is a test sample:

@ContextConfiguration( classes = CollectionConfig.class )
@DirtiesContext( classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD )
public class DaoMappingTest extends AbstractTestNGSpringContextTests {

@Autowired
private SessionFactory testSessionFactory;

@Test
public void thatMovieIsSaved() {
    Movie killBill = getKillBillMovie0();

    saveToDb( Arrays.asList(killBill) );

    Session querySession = testSessionFactory.openSession();
    List<Movie> movies = querySession.createQuery( "from Movie" ).list();
    querySession.close();

    assertThat( movies ).containsExactly( killBill );
}

@Test
public void that2MoviesIsSaved() {
    Movie killBill = getKillBillMovie0();
    Movie terminator = getTerminatorMovie1();

    saveToDb( Arrays.asList( killBill, terminator ) );

    Session querySession = testSessionFactory.openSession();
    List<Movie> movies = querySession.createQuery( "from Movie" ).list();
    querySession.close();

    assertThat( movies ).containsOnly( killBill, terminator );
}

private void saveToDb( List<?> objects ) {
    Session session = testSessionFactory.openSession();
    session.beginTransaction();

    for( Object obj : objects) {
        session.save( obj );
    }

    session.getTransaction().commit();
    session.close();
}

@AfterSuite
public void tearDown() {
    testSessionFactory.close();
}
}


回答4:

It looks to me like you have a reproducible bug in the Hibernate DDL creation code. You should report a bug - it's a long term solution but it's the way things are done in open source. Of course you might want to produce a patch, but I never found the Hibernate code base easy to hack.



回答5:

I ran into the same problem where MS SQL Server wants the catalog and schema defined, but HSQLDB does not. My solution was to load a custom orm.xml file (via persistence.xml) specifically for MS SQL Server that sets the catalog and schema.

1.Only specify the @Table name (omit any catalog or schema info) for your entity:

@Entity
@Table(name="CATEGORY")
public static class Category { ... }

2.Specify two persistence-unit nodes in your META-INF/persistence.xml file

<persistence version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <!--
     | For production and integration testing we use MS SQL Server, which needs  
     | the catalog and schema set (see orm-mssql.xml).
     |-->
    <persistence-unit name="com.mycompany.prod">
        <mapping-file>META-INF/orm-mssql.xml</mapping-file>
    </persistence-unit>

    <!--
     | For unit testing we use HSQLDB, which does not need the catalog or schema.
     |-->
    <persistence-unit name="com.mycompany.test" />

</persistence>

3.Specify the default catalog and schema in the orm-mssql.xml file:

<entity-mappings version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence/orm" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd">

    <persistence-unit-metadata>

        <!--
         | Set the catalog and schema for MS SQL Server
         |-->
        <persistence-unit-defaults>
            <schema>MYSCHEMA</schema>
            <catalog>MYCATALOG</catalog>
        </persistence-unit-defaults>

    </persistence-unit-metadata>

</entity-mappings>

4.I'm using Spring to configure JPA, so I use a property-placeholder for the value of the persistenceUnitName:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter" />
    <property name="persistenceUnitName" value="${entityManagerFactory.persistenceUnitName}" />
</bean>

For unit tests, use 'com.mycompany.test' and for integration-tests/production deployments, use 'com.mycompany.prod'.