Plugin api design (multiple domain objects)

Flyway is a library for performing database migrations and the Gradle plugin supports performing these as a build step. I wrote this plugin for performing code generation against an H2 schema, with the release process using Flyway outside of Gradle. Understandably most users want to leverage the plugin for their release process - a common idiom in Maven.

The plugin currently supports single database migrations using the flyway extension. A pull request adds the ability to migrate multiple independent databases, e.g. transactional and reporting database instances. This is not a database per environment (e.g. prod, staging), but multiple databases in a single environment. The open question is, what is the proper approach for a configuration DSL?

Currently there are three proposals:

  1. flyway and flywayMulti container

If only the flyway extension is defined, then it behaves in a single database mode and that configuration is used. If the flywayMulti extension is also found, then the flyway extension is treated as default values and each container entry is the per-database overrides.

flyway {
  driver = 'org.h2.Driver'
  user = 'SA'
  password = 'mySecretPwd'
  schemaGenericFirst = true
}
flywayMulti {
  TransactionalDB {
    url = "jdbc:h2:${buildDir}/db/flyway/transaction"
     sqlMigrationPrefix = 'Transaction-'
    sqlMigrationSuffix = '-OK.sql'
  }
  ReportingDB {
    url = "jdbc:h2:${buildDir}/db/flyway/report"
     sqlMigrationPrefix = 'Reporting-'
    sqlMigrationSuffix = '-OK.sql'
  }
}
  1. flyway container and flywayDefaults extension

This is the inverse of the above scheme. It has the advantage of consistency where the container must be defined for a single entry, rather than conditionally treating an extension as defaults. The disadvantage is that the common case of a single database requires a single entry wrapper.

flywayDefaults {
  driver = 'org.h2.Driver'
  user = 'SA'
  password = 'mySecretPwd'
  schemaGenericFirst = true
}
flyway {
  TransactionalDB {
    url = "jdbc:h2:${buildDir}/db/flyway/transaction"
     sqlMigrationPrefix = 'Transaction-'
    sqlMigrationSuffix = '-OK.sql'
  }
  ReportingDB {
    url = "jdbc:h2:${buildDir}/db/flyway/report"
     sqlMigrationPrefix = 'Reporting-'
    sqlMigrationSuffix = '-OK.sql'
  }
}
  1. flyway container with default item

This approach is like (2), except that it merges the two extensions into one. It requires care because container functionality must be filtered to not run the default entry.

flyway {
  defaults {
    driver = 'org.h2.Driver'
    user = 'SA'
    password = 'mySecretPwd'
    schemaGenericFirst = true
  }
  TransactionalDB {
    url = "jdbc:h2:${buildDir}/db/flyway/transaction"
     sqlMigrationPrefix = 'Transaction-'
    sqlMigrationSuffix = '-OK.sql'
  }
  ReportingDB {
    url = "jdbc:h2:${buildDir}/db/flyway/report"
     sqlMigrationPrefix = 'Reporting-'
    sqlMigrationSuffix = '-OK.sql'
  }
}

Which approach is preferred or are there alternatives that we haven’t considered? One answer may be that none are if this type of usage of Gradle (release process) is frowned upon?

Interesting stuff. Any kind of automation is fair game in Gradle. This is a great use case.

My preference would be for something like #3, but I’d create another nested element…

flyway {
  defaults {
    driver = 'org.h2.Driver'
    user = 'SA'
    password = 'mySecretPwd'
    schemaGenericFirst = true
  }
  dbs {
    transactional {
      url = "jdbc:h2:${buildDir}/db/flyway/transaction"
       sqlMigrationPrefix = 'Transaction-'
      sqlMigrationSuffix = '-OK.sql'
    }
    reporting {
      url = "jdbc:h2:${buildDir}/db/flyway/report"
       sqlMigrationPrefix = 'Reporting-'
      sqlMigrationSuffix = '-OK.sql'
    }
    }
}

This does make the simple case slightly more verbose.

I’s solve this by breaking the plugin up into multiple plugins.

  • flyway-base * flyway-single

The “base” plugin just adds the DSL extensions, without any conventions. The “single” plugin preconfigures a single db instance and possible other conventions.

If your worried about conciseness. The single plugin can add another “flywaySingle” extension which is just the “main” db…

class FlywaySinglePlugin implements Plugin<Project> {
  void apply(Project project) {
    project.apply(plugin: FlywayBasePlugin)
    def flywayDbs = project.flyway.dbs
    def mainDb = flywayDbs.create("main") {
      // any default config
    }
          project.extensions.add("flywaySingle", mainDb)
  }
}

That’s just a quick code sketch.

Thanks Luke! I like your proposal a lot. We’ll give it a shot.

Great.

Please let me know how it works out. This will be a useful plugin to let the world know about.

Luke,

We released v0.6 with the configuration language you suggested, but skipped the ‘flywaySingle’ suggestion as it appeared concise enough. Thanks for the tips!