Write a Custom Bazel Rule for Hibernate DDL

Jack Zhai
5 min readJun 20, 2023

Synopsis

I am working a Java project built by Bazel. The DDL(Data Definition Language) of its JPA Entity class have to be generated via Bazel.

But I found that there’s no any Bazel rule for that. So I decided that write one for it, although I am not good at Bazel at that moment.

There’re two parts I have to learn:

  1. How Hibernate generate DDL? I learnt it from Maven plugin of Hibernate DDL.
  2. How to write a custom Bazel rule to run the logic of Hibernate DDL? This is the most difficult part cause by just a little bit information about it on the Internet.

Let’s go through these two parts.

The versions of tools in this blog as following:

  • Hibernate 6.2.0.Final
  • Bazel 5.4.0

Part 1: How Hibernate generate DDL

Hibernate provides a module named Hibernate ORM Hibernate Ant, where is the place of the logic of generating DDL.

The main logic is simple:

SchemaExport schemaExport = new SchemaExport();  
// For generating a export script file, this is the file which will be written.
schemaExport.setOutputFile(outputFile);
// Set the end of statement delimiter
schemaExport.setDelimiter(delimiter);
// Should we stop once an error occurs?
schemaExport.setHaltOnError(haltOnError);
// Should we format the sql strings?
schemaExport.setFormat(format);
schemaExport.execute(EnumSet.of(TargetType.SCRIPT), SchemaExport.Action.CREATE, metadata.buildMetadata());

SchemaExport is a class in Hibernate ORM Hibernate Ant module. More arguments.

The first argument value EnumSet.of(TargetType.SCRIPT) of execute method of SchemaExport presents SchemaExport only generate sql script somewhere, not to run it to a database instance.

The second argument value SchemaExport.Action.CREATE presents only generate create statements.

The third argument metadata.buildMetadata() presents the some information SchemaExport need. We We prepared it as following:

Map<String, Object> settings = new HashMap<>();  
settings.put("hibernate.dialect", dialect);
// all we need is tell serviceRegistry that the sql dialect we use.
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(settings).build();
MetadataSources metadata = new MetadataSources(serviceRegistry);
// add Entity classes to metadata so that Hibernate is able to know which class wanna be generated DDL SQL.
metadata.addAnnotatedClasses(allClassOf(packageStr));

The logic is clear, let’s have a summary of it:

  1. Prepare some settings via MetadataSources.
  2. Execute a SchemaExport instance.

Part 2: How to write a custom Bazel rule

Actually, there’re many concepts behind Bazel have to learn, if you wanna write a custom Bazel rule, like toolchain, runfiles, depset, provider etc.

This blog not going to explain them. You can learn them from Bazel’s documentation.

Step 1: Prepare WORKSPACE and the folder

In WORKSPACE, we use rules_jvm_external to manage the dependencies:

HBERNATE_VERSION = "6.2.0.Final"
maven_install(
artifacts = [
"org.reflections:reflections:0.10.2",
"jakarta.persistence:jakarta.persistence-api:3.1.0",
"org.hibernate.orm:hibernate-core:%s" % HBERNATE_VERSION,
"org.hibernate.orm:hibernate-ant:%s" % HBERNATE_VERSION,
]
)

Then, we create a subfolder to store the custom rule file in own project folder as following:

% tree tools/rules_hibernate
tools/rules_hibernate
├── BUILD.bazel
├── internal
│ └── SchemaGeneratorCommand.java
└── rules.bzl

Step 2: Implement DDL Generation Logic

As you see, we implement DDL generation logic in a Java file SchemaGeneratorCommand.java. All things implement in the main method.

package internal;  

import jakarta.persistence.Entity;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.schema.TargetType;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.reflections.scanners.Scanners.SubTypes;
import static org.reflections.scanners.Scanners.TypesAnnotated;

public class SchemaGeneratorCommand {

public static void main(String... args) {
Logger.getLogger("org.hibernate").setLevel(Level.SEVERE);

try {
Map<String, String> params = params(args);
String outputFile = params.get("outputFile");
String packageStr = params.get("packages");
boolean haltOnError = Boolean.parseBoolean(params.get("haltOnError"));
String delimiter = params.get("delimiter");
String dialect = params.get("dialect");
boolean format = Boolean.parseBoolean(params.get("format"));

Map<String, Object> settings = new HashMap<>();
settings.put("hibernate.dialect", dialect);
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(settings).build();
MetadataSources metadata = new MetadataSources(serviceRegistry);
metadata.addAnnotatedClasses(allClassOf(packageStr));

SchemaExport schemaExport = new SchemaExport();
schemaExport.setOutputFile(outputFile);
schemaExport.setDelimiter(delimiter);
schemaExport.setHaltOnError(haltOnError);
schemaExport.setFormat(format);
schemaExport.execute(EnumSet.of(TargetType.SCRIPT), SchemaExport.Action.CREATE, metadata.buildMetadata());
System.exit(0);
} catch (Exception e) {
throw new RuntimeException("generate ddl error", e);
}

}
private static Class<?>[] allClassOf(String packageStr) {
Reflections reflections = new Reflections(new ConfigurationBuilder().forPackages(packageStr.split(",")));
Set<Class<?>> annotated =
reflections.get(SubTypes.of(TypesAnnotated.with(Entity.class)).asClass());

if (annotated == null || annotated.size() == 0) {
throw new RuntimeException("generate ddl error, there's no entity in the packages:" + packageStr);
}
return annotated.toArray(new Class[annotated.size()]);
}


public static Map<String, String> params(String... args) {
int length = args.length;

if (length % 2 != 0) {
throw new RuntimeException("params() must be called with key/value pairs");
}

Map<String, String> map = new HashMap<String, String>(length / 2);

for (int i = 0; i < length; i += 2) {
String key = args[i];
String value = args[i + 1];
System.out.println("key:" + key + ",value:" + value);
map.put(key, value);
}
return map;
}
}

Step 3: Build SchemaGeneratorCommand.java

We have to build it, so that Bazel can run it. The content of BUILD.bazel as following:

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

java_binary(
name = "SchemaGeneratorCommand",
main_class = "internal.SchemaGeneratorCommand",
srcs = ["SchemaGeneratorCommand.java"],
deps = [
"@maven//:org_reflections_reflections",
"@maven//:org_hibernate_orm_hibernate_ant",
"@maven//:jakarta_persistence_jakarta_persistence_api",
"@maven//:org_hibernate_orm_hibernate_core",
],
)

Step 4: Write a Custom Bazel Rule

The custom rule’s logic follow these steps implemented in _hibernate_ddl_impl function:

  1. Read the attributes from Bazel ctx object.
  2. Pass the args to SchemaGeneratorCommand binary via ctx.actions.run
def _hibernate_ddl_impl(ctx):  
output = ctx.actions.declare_file(ctx.label.name + ".sql")
entity_lib_path = ctx.file.entity_lib.path
# args for SchemaGeneratorCommand's main method
args = ctx.actions.args()

jar_delimiter = ":"
if ctx.attr.is_windows:
jar_delimiter = ";"

class_path = jar_delimiter.join([f.path for f in ctx.attr.entity_lib[JavaInfo].full_compile_jars.to_list()]
+ [f.path for f in ctx.attr.entity_lib[JavaInfo].transitive_compile_time_jars.to_list()])
# append jars for the javac command
args.add("--main_advice_classpath=" + class_path)
args.add('dialect', ctx.attr.dialect)
args.add('format', ctx.attr.format)
args.add('delimiter', ctx.attr.delimiter)
args.add('outputFile', output.path)
args.add('haltOnError', ctx.attr.haltOnError)
args.add('packages', ",".join([package for package in ctx.attr.packages]))

runfiles = ctx.runfiles(files = [ctx.file.entity_lib]+ [output])

ctx.actions.run(
inputs = depset(
direct = [ctx.file.entity_lib],
transitive = [ctx.attr.entity_lib[JavaInfo].transitive_compile_time_jars]
),
outputs = [output],
arguments = [args],
progress_message = "Generate sql from entity:{} to {}".format(entity_lib_path, output.path),
executable = ctx.executable._executable,
mnemonic = "HibernateSchemaDDL",
)
return [
DefaultInfo(
runfiles = runfiles,
files = depset([output])
)]

_hibernate_ddl = rule(
implementation = _hibernate_ddl_impl,
attrs = {
"is_windows": attr.bool(mandatory = True),
"format": attr.bool(
default = True,
),
"haltOnError": attr.bool(
default = True,
),
"delimiter": attr.string(
default = ";"
),
"dialect": attr.string(
mandatory = True,
),
"packages":attr.string_list(
mandatory = True,
),
"entity_lib":attr.label(
mandatory = True,
allow_single_file = True,
),
"_executable": attr.label(
cfg="host",
executable = True,
# important!
default="//tools/rules_hibernate:SchemaGeneratorCommand"
),
},
fragments = ["java"],
)


def hibernate_ddl(name, **kwargs):
_hibernate_ddl(
name = name,
is_windows = select({
"@bazel_tools//src/conditions:windows": True,
"//conditions:default": False,
}
),
**kwargs
)

Use hibernate_ddl

Finally, we can use the rule in somewhere:

load("//tools/rules_hibernate:rules.bzl", "hibernate_ddl")
hibernate_ddl (
name = "incident_ddl",
entity_lib = "//core/src/main/java/example/incident:incident",
packages = ["example.incident"],
dialect = "org.hibernate.dialect.PostgreSQLDialect"
)

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

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

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