Using Ebean and Bazel for Database DDL Generation

Jack Zhai
4 min readOct 19, 2023

--

What using Ebean

Ebean is a JPA implementation. It provides various useful mapping annotations and more simpler use than Hibernate another famous JPA implement.

Why using Bazel to generate DDLs?

Because everything is built by Bazel in my project include the DDL(Data Definition Language) that have to be generated from Java entities.

And It haven’t this kind of rules for Ebean.

How to implement it?

Ebean providers DbMigration class that implement DDL’s generation from Java entity. So we just need to figure out a method to call the function of DbMigration to generate DDLs. And It’s an easy job for Bazel as it could be implemented as an executable rule.

The steps to generate DDLs

  1. configure external dependencies in WORKSPACE
  2. implement the Ebean DDL generator of java_binary
  3. run the java_binary to generate DDL before git submit

Step 1: Configure external dependencies in WORKSPACE

I use maven_install function provided from rules_jvm_external to download Ebean's dependencies.

# setup rules_jvm_external
# download ebean-agent
JACKSON_VERSION = "2.15.2"
EBEAN_VERSION = "13.18.0-jakarta"
maven_install(
artifacts = [

# other dependencies...
"jakarta.persistence:jakarta.persistence-api:3.1.0",
"com.zaxxer:HikariCP:5.0.1",

# ebean
'io.ebean:ebean-api:%s' % EBEAN_VERSION,
'io.ebean:ebean-platform-all:%s' % EBEAN_VERSION,
'io.ebean:ebean-ddl-generator:%s' % EBEAN_VERSION,
'io.ebean:ebean-core:%s' % EBEAN_VERSION,
'io.ebean:ebean-core-type:%s' % EBEAN_VERSION,
'io.ebean:ebean-jackson-mapper:%s' % EBEAN_VERSION,
'io.ebean:ebean-annotation:%s' % "8.4",
'io.ebean:ebean-agent:%s' % "13.20.1",
'io.ebean:ebean-migration:%s' % "13.9.0",
'io.ebean:ebean-ddl-runner:%s' % '2.3',
'io.ebean:ebean-datasource:%s' % "8.5",
'io.ebean:ebean-postgres:%s' % EBEAN_VERSION,
# integrate postgresql, you can change it to other database
'io.ebean:ebean-platform-postgres:%s' % EBEAN_VERSION,
# utils
"commons-cli:commons-cli:1.5.0",


# json
"com.fasterxml.jackson.core:jackson-core:%s" % JACKSON_VERSION,
"com.fasterxml.jackson.core:jackson-databind:%s" % JACKSON_VERSION,
'com.fasterxml.jackson.core:jackson-annotations:%s' % JACKSON_VERSION,
'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:%s' % JACKSON_VERSION,

# jdbc
"org.postgresql:postgresql:42.6.0",

# for test environment
"org.testcontainers:testcontainers:1.19.1",
"org.testcontainers:postgresql:1.19.1",
"com.github.docker-java:docker-java-api:3.3.0",

],
fetch_sources = False,
maven_install_json = "//:maven_install.json",
repositories = [
"https://repo1.maven.org/maven2",
],
version_conflict_policy = "pinned",
)

load("@maven//:defs.bzl", "pinned_maven_install")

pinned_maven_install()

Step 2: Implement the Ebean DDL generator of java_binary

We attempt to implementation of the ebean_ddl java_binary target that can be run as a command like: bazel run -- //tools/rules_ebean/src/main/java/internal:ebean_ddl -p=$PWD/repository-impl/src/main/sql -g="codes.showme.domain"

-p parameter tells the implement of ebean_ddl java_binary target where DDL files generated by Ebean DbMigration class should be placed.
-g parameter represents which packages should be scanned by Ebean DbMigration class.

The structure of folder of implement is as below:

> $ tree tools                            
tools
├── rules_ebean
│ ├── BUILD.bazel
│ └── src
│ ├── META-INF
│ │ └── services
│ │ ├── BUILD.bazel
│ │ ├── io.ebean.dbmigration.DbMigration
│ │ └── io.ebeaninternal.api.SpiDdlGeneratorProvider
│ └── main
│ └── java
│ └── internal
│ ├── BUILD.bazel
│ └── DdlGeneratorCommand.java

The META-INF folder and the insides of it is required by DbMigration class, cause by DbMigration uses the SPI technique.

The each content of them is as below:

tools/rules_ebean/src/META-INF/services/BUILD.bazel:

filegroup(  
name = "ebean_spi",
srcs = [
"io.ebeaninternal.api.SpiDdlGeneratorProvider",
"io.ebean.dbmigration.DbMigration"
],
visibility = ["//visibility:public"],
)

io.ebean.dbmigration.DbMigration:
io.ebeaninternal.dbmigration.DefaultDbMigration

io.ebeaninternal.api.SpiDdlGeneratorProvider:
io.ebeaninternal.dbmigration.DdlGeneratorProvider

Then let’s go inside the core of implementation.

tools/rules_ebean/src/main/java/internal/BUILD.bazel:

package(default_visibility = ["//visibility:public"])  

java_binary(
name = "ebean_ddl",
main_class = "internal.DdlGeneratorCommand",
srcs = ["//tools/rules_ebean/src/main/java/internal:DdlGeneratorCommand.java"],
resources = ["//tools/rules_ebean/src/META-INF/services:ebean_spi"],
data = ["@maven//:io_ebean_ebean_agent"],
# Every entity has to be enhance by ebean agent
jvm_flags = ["-javaagent:$(location @maven//:io_ebean_ebean_agent)"],
deps = [
# Java entities deps have to be here, like:
"//core/src/main/java/codes/example/entities/incident",

# other dependencies of Ebean、commons cli、test container、jackson
],
)

tools/rules_ebean/src/main/java/internal/DdlGeneratorCommand.java:

package internal;  
public class DdlGeneratorCommand {
public static final String TESTS_DB = "ebean_ddlgeneratorcommand";
public static final String DB_SCHEMA = "public";
public static final String META_TABLE = "db_migrations";
public static final String MIGRATION_PATH = "domain";
public static final int STARTUP_TIMEOUT_SECONDS = 60;
private static Options OPTIONS = new Options();
private static CommandLine commandLine;
private static String HELP_STRING = null;

public static void main(String[] args) throws ParseException {

CommandLineParser commandLineParser = new DefaultParser();
// help
OPTIONS.addOption("help","usage help");
// port
OPTIONS.addOption(Option.builder("p").required().hasArg(true).longOpt("path")
.type(String.class).desc("the path of where model is").build());
OPTIONS.addOption(Option.builder("g").required().type(String.class)
.hasArgs().longOpt("packages").desc("the packages would be scan by Ebean").build());
try {
commandLine = commandLineParser.parse(OPTIONS, args);
} catch (ParseException e) {
System.out.println(e.getMessage() + "\n" + getHelpString());
System.exit(0);
}
// setup a postgres container
try (PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:12.8")
.withDatabaseName(TESTS_DB)
.withUsername("postgres")
.withPassword("postgres")

) {
postgreSQLContainer.withStartupTimeoutSeconds(STARTUP_TIMEOUT_SECONDS).start();
String sqlBasePath = commandLine.getOptionValue("path");
String[] packages = commandLine.getOptionValues("packages");

HikariDataSource datasource = new HikariDataSource();
datasource.setJdbcUrl(postgreSQLContainer.getJdbcUrl());

datasource.setUsername(postgreSQLContainer.getUsername());
datasource.setPassword(postgreSQLContainer.getPassword());
datasource.setAutoCommit(true);

DatabaseConfig config = new DatabaseConfig();
config.setName("domain-sql");
config.setRegister(true);
config.setDefaultServer(true);
config.setDdlExtra(false);
config.setPackages(Arrays.asList(packages));
config.setTenantMode(TenantMode.PARTITION);
config.setCurrentTenantProvider(new CurrentTenant());
config.setDdlCreateOnly(false);
config.setDdlGenerate(true);
config.setDdlRun(false);
config.setDbSchema(DB_SCHEMA);
config.setDataSource(datasource);

// important!!! this step is import, because it new a database instance for the dbserver provider
// that DbMigration have to gather.
Database database = DatabaseFactory.create(config);
DbMigration dbMigration = DbMigration.create();
dbMigration.setServerConfig(config);
dbMigration.setStrictMode(false);
dbMigration.setPlatform(new PostgresPlatform());

dbMigration.setMigrationPath(MIGRATION_PATH);
dbMigration.setPathToResources(sqlBasePath);
String generateInitMigration = dbMigration.generateInitMigration();
String reuslt = dbMigration.generateMigration();

MigrationConfig migrationConfig = new MigrationConfig();

migrationConfig.setDbSchema(DB_SCHEMA);
migrationConfig.setMetaTable(META_TABLE);
migrationConfig.setMigrationPath("filesystem:" + sqlBasePath + File.separator + MIGRATION_PATH);

// just for have a test for the sqls
MigrationRunner runner = new MigrationRunner(migrationConfig);
runner.run(datasource);

} catch (IOException e) {
throw new RuntimeException(e);
}
}

private static String getHelpString() {
if (HELP_STRING == null) {
HelpFormatter helpFormatter = new HelpFormatter();

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintWriter printWriter = new PrintWriter(byteArrayOutputStream);
helpFormatter.printHelp(printWriter, HelpFormatter.DEFAULT_WIDTH, "scp -help", null,
OPTIONS, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, null);
printWriter.flush();
HELP_STRING = new String(byteArrayOutputStream.toByteArray());
printWriter.close();
}
return HELP_STRING;
}
}

Step 3: Run the java_binary to generate DDL before git commit

we can add the command bazel run -- //tools/rules_ebean/src/main/java/internal:ebean_ddl -p=$PWD/repository-impl/src/main/sql -g="codes.showme.domain" to the config of pre-commit so that we don't miss any DDL change.

Conclusion

One power of Bazel is that it’s very easy to implement your eager build function, like the Ebean DDL migration functionality which the Ebean maintainer does not provide.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Jack Zhai
Jack Zhai

Written by Jack Zhai

DevOps,SRE,Bazel The Author of 《Jenkins2.x In Practice》, https://showme.codes

No responses yet

Write a response