Is it possible to define a changeset programmatically?

Hi,

I would like to use Liquibase for inserting master data in a JPA environment, so I have no need to define SQL statements
or using in changesets. I would prefer to define a changeset programatically, e.g. as a Java class implementing
a special interface or as a JUnit Testcase (because then I can use SpringRunner and the like to get the PersistenceContext)
This way I could use JPA for creating the data, keep the refactoring support of my IDE and use LiquiBase just for the
book keeping.
So, is something like this already supported? If not, do you think some kind of JUnit extension is easy to implement?
(would also be nice for defining prerequisites)

To make myself clear, I want to define something like this:
 
     
 

regards,

Robin.

There is some support for this.  For liquibase 1.9 and before, there is custom refactoring class support (http://www.liquibase.org/manual/custom_refactoring_class).

In your example, you can do:

       

by having InsertNewStates implement liquibase.change.custom.CustomSqlChange or liquibase.change.custom.CustomTaskChange

For 2.0 and beyond, there is a much improved extension system (http://liquibase.jira.com/wiki/display/CONTRIB/LiquiBase+Extensions+Portal) which gives you much more control.

Nathan

Thanks Nathan, this seems like a possible solution!
But now I’m having classloading issues with LiquiBase (1.9.5) when trying to run a custom change via LiquibaseServletListener:

  1. LiquibaseServletListener  adds a log Handler which uses LiquibaseStatusServlet class. (-> NoClassDefFoundError)
        LogFactory.getLogger().addHandler(new Handler() {
                public synchronized void publish(LogRecord record) {
                    LiquibaseStatusServlet.logMessage(record);
                }
                …
        });

    The LogFactory does not find the servlet class, I guess it searches the system classpath only.
    I could fix this by using reflection to invoke LiquibaseStatusServlet.logMessage() but then the next problem arose:

  1. CustomChangeWrapper.setClass() uses Class.forName() to resolve the custom change class.
        Since Class.forName() uses the Classloader of the calling class, it doesn’t find my custom change class, because
        I have an EAR and Liquibase is in a hierarchically “higher” classloader than my model classes.
        Can you change this to Thread.currentThread().getContextClassLoader().loadClass() ?
        Or maybe this is already changed in 2.0?

I tried 2-0-rc2 but then liquibase.FileOpener was not found, so I think the CustomChange interface changed.
BTW, is there a public SVN for LiquiBase? I would like to take a look into the 2.0 sources.

thanks,

Robin.

Managing all the different ways classloaders can be used does run into problems.  What app server are you running in?

I did make some changes around how classloaders are managed in 2.0.  The SVN repository can be checked out from http://liquibase.jira.com/svn/CORE/trunk  if you want to check out the source.  If you find the problem, you can let me know or commit a change and I will look it over.

Nathan

Yes, but Class.forName(String) is wrong in almost any case, especially when loading classes from a different JAR like
in this scenario. I’m currently using Glassfish V2 but migration to V3 is planned for the next month.
I checked out the code and saw that CustomChangeWrapper does provide a setClassLoader() method and first tries
to use Class.forName(className, true, classLoader), but I can’t imagine how to provide a classloader via XML.
Maybe a convention like classLoader=“CONTEXT” could be used or Thread.currentThread().getContextClassLoader()
used as a default. (which would be the right choice for almost any case)

Robin.

I updated the CustomChangeWrapper class in 2.0 to use this:

      public void setClass(String className) throws CustomChangeException {         this.className = className;         try {             try {                 customChange = (CustomChange) Class.forName(className, true, classLoader).newInstance();             } catch (ClassCastException e) { //fails in Ant in particular                 try {                     customChange = (CustomChange) Thread.currentThread().getContextClassLoader().loadClass(className).newInstance();                 } catch (ClassNotFoundException e1) {                     customChange = (CustomChange) Class.forName(className).newInstance();                 }             }         } catch (Exception e) {             throw new CustomChangeException(e);         }     }

That seems like it should catch all the cases.

Nathan