
org.dellroad.stuff.spring.SpringSQLSchemaUpdater Maven / Gradle / Ivy
Show all versions of dellroad-stuff-spring Show documentation
/*
* Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.spring;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Comparator;
import org.dellroad.stuff.schema.DatabaseAction;
import org.dellroad.stuff.schema.SQLCommand;
import org.dellroad.stuff.schema.SQLSchemaUpdater;
import org.dellroad.stuff.schema.SchemaUpdate;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
/**
* {@link SQLSchemaUpdater} optimized for use with Spring.
*
*
*
*
*
*
* - {@link #apply(Connection, DatabaseAction) apply()} is overridden so Spring {@link DataAccessException}s are thrown.
* - {@link #indicatesUninitializedDatabase indicatesUninitializedDatabase()} is overridden to examine exceptions
* and more precisely using Spring's exception translation infrastructure to filter out false positives.
* - {@link #getOrderingTieBreaker} is overridden to break ties by ordering updates in the same order
* as they are defined in the bean factory.
* - This class implements {@link InitializingBean} and verifies all required properties are set.
* - If no updates are {@linkplain #setUpdates explicitly configured}, then all {@link SpringSQLSchemaUpdate}s found
* in the containing bean factory are automatically configured.
*
*
*
* An example of how this class can be combined with custom XML to define an updater, all its updates,
* and a {@link org.dellroad.stuff.schema.SchemaUpdatingDataSource} that automatically updates the database schema:
*
* <beans xmlns="http://www.springframework.org/schema/beans"
* xmlns:dellroad-stuff="http://dellroad-stuff.googlecode.com/schema/dellroad-stuff"
* xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
* xmlns:p="http://www.springframework.org/schema/p"
* xsi:schemaLocation="
* http://www.springframework.org/schema/beans
* http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
* http://dellroad-stuff.googlecode.com/schema/dellroad-stuff
* http://dellroad-stuff.googlecode.com/svn/wiki/schemas/dellroad-stuff-1.0.xsd">
*
* <!-- DataSource that automatically updates the database schema -->
* <bean id="dataSource" class="org.dellroad.stuff.schema.SchemaUpdatingDataSource"
* p:dataSource-ref="realDataSource" p:schemaUpdater-ref="schemaUpdater"/>
*
* <!--
* Database updater bean. This is used on first access to the DataSource above. Notes:
* - "databaseInitialization" is used to initialize the schema (first time only)
* - "updateTableInitialization" is used to initialize the update table (first time only)
* - In this example, we just use dellroad-stuff's update table initialization for MySQL
* - The <dellroad-stuff:sql-update> beans below will be auto-detected
* -->
* <bean id="schemaUpdater" class="org.dellroad.stuff.spring.SpringSQLSchemaUpdater">
* <property name="databaseInitialization">
* <dellroad-stuff:sql resource="classpath:databaseInit.sql"/>
* </property>
* <property name="updateTableInitialization">
* <dellroad-stuff:sql resource="classpath:org/dellroad/stuff/schema/updateTable-mysql.sql"/>
* </property>
* </bean>
*
* <!-- Schema update to add the 'phone' column to the 'User' table -->
* <dellroad-stuff:sql-update id="addPhone">ALTER TABLE User ADD phone VARCHAR(64)</dellroad-stuff:sql-update>
*
* <!-- Schema update to run some complicated external SQL script -->
* <dellroad-stuff:sql-update id="majorChanges" depends-on="addPhone" resource="classpath:majorChanges.sql"/>
*
* <!-- Multiple SQL commands that will be automatically separated into distinct updates -->
* <dellroad-stuff:sql-update id="renameColumn">
* ALTER TABLE User ADD newName VARCHAR(64);
* ALTER TABLE User SET newName = oldName;
* ALTER TABLE User DROP oldName;
* </dellroad-stuff:sql-update>
*
* <!-- Add more schema updates over time as needed and everything just works... -->
*
* </beans>
*
*
*
* In the case no schema updates are explicitly configured, it is required that this updater and all of its
* schema updates are defined in the same {@link ListableBeanFactory}.
*/
public class SpringSQLSchemaUpdater extends SQLSchemaUpdater implements BeanFactoryAware, InitializingBean {
private ListableBeanFactory beanFactory;
@Override
public void afterPropertiesSet() throws Exception {
if (this.getDatabaseInitialization() == null)
throw new Exception("no database initialization configured");
if (this.getUpdateTableInitialization() == null)
throw new Exception("no update table initialization configured");
if (this.getUpdates() == null) {
if (this.beanFactory == null) {
throw new IllegalArgumentException("no updates explicitly configured and the containing BeanFactory"
+ " is not a ListableBeanFactory: " + this.beanFactory);
}
this.setUpdates(this.beanFactory.getBeansOfType(SpringSQLSchemaUpdate.class).values());
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (beanFactory instanceof ListableBeanFactory)
this.beanFactory = (ListableBeanFactory)beanFactory;
}
/**
* Determine if an exception thrown during {@link #databaseNeedsInitialization} is consistent with
* an uninitialized database.
*
*
* The implementation in {@link SpringSQLSchemaUpdater} looks for a {@link BadSqlGrammarException}.
*/
@Override
protected boolean indicatesUninitializedDatabase(Connection c, SQLException e) throws SQLException {
return this.translate(e, c, null) instanceof BadSqlGrammarException;
}
/**
* Apply a {@link DatabaseAction} to a {@link Connection}.
*
*
* The implementation in {@link SQLSchemaUpdater} invokes the action and delegates to
* {@link #translate(SQLException, Connection, String) translate()} to convert any {@link SQLException} thrown.
*
* @throws SQLException if an error occurs attempting to translate a thrown SQLException
* @throws DataAccessException if an error occurs accessing the database
* @see #translate(SQLException, Connection, String) translate()
*/
@Override
protected void apply(Connection c, DatabaseAction action) throws SQLException {
try {
super.apply(c, action);
} catch (SQLException e) {
String sql = action instanceof SQLCommand ? ((SQLCommand)action).getSQL() : null;
throw this.translate(e, c, sql);
}
}
/**
* Converts {@link SQLException}s into Spring {@link DataAccessException}s.
*
* @param e original exception
* @param c the connection on which the exception coccurred
* @param sql the SQL statement that generated the exception
* @return the corresponding Spring {@link DataAccessException}
* @throws SQLException if exception translation fails
*/
protected DataAccessException translate(SQLException e, Connection c, String sql) throws SQLException {
return new SQLErrorCodeSQLExceptionTranslator(c.getMetaData().getDatabaseProductName())
.translate("database access during schema update", sql, e);
}
/**
* Get the preferred ordering of two updates that do not have any predecessor constraints
* (including implied indirect constraints) between them.
*
*
* In the case no schema updates are explicitly configured, the {@link Comparator} returned by the
* implementation in {@link SpringSQLSchemaUpdater} sorts updates in the same order that they appear
* in the containing {@link ListableBeanFactory}. Otherwise, the
* {@linkplain org.dellroad.stuff.schema.AbstractSchemaUpdater#getOrderingTieBreaker superclass method} is used.
*/
@Override
protected Comparator> getOrderingTieBreaker() {
if (this.beanFactory == null)
return super.getOrderingTieBreaker();
final BeanNameComparator beanNameComparator = new BeanNameComparator(this.beanFactory);
return new Comparator>() {
@Override
public int compare(SchemaUpdate update1, SchemaUpdate update2) {
return beanNameComparator.compare(update1.getName(), update2.getName());
}
};
}
}