Open landsman opened 1 year ago
I've just finished setting the same stack up and have gotten it all working without exceptions, it also took me several hours due to a complete lack of documentation/info on how to setup the different plugins all together.
I'd also be happy to help work on documentation for this stack as there is surprisingly little info on how to make it work.
@ZooToby yes, please! What about starting with a quick YouTube video where you show it and the community can help thanks to that?
I ended up with JPA Buddy to avoid this pain.
I'd be happy to - unless it may be more helpful for be to post my config/learnings in a quick write up here?
The final config is actually fairly simple, just took a heap of debugging/trawling through source code to get it working
Even got it setup to automatically generate change logs following an alphabetic naming scheme so I'm pretty happy with the final config
That sounds good. I'd like to take a look at the configuration and provide some feedback.
Okay, here we go:
Quick note; I haven't setup liquibase kotlin dsl plugin as we already use yaml for a lot of our IaC/other config
I've only included what I thought is relevant, am happy to update with the rest of the projects structure if it makes things clearer
src
main
kotlin
resources
application.yml
db
liquibase.properties
master-changelog.yaml
changelogs
00.creating-types.changelog.yaml
01.creating-tables.changelog.yaml
dataseeds
00.seeding-users.dataseed.yaml
build.gradle.kts
.gradle
The first part of the config is fairly standard
plugins {
val kotlinVersion = "1.9.0"
val springBootVersion = "3.1.4"
val springDependencyManagementVersion = "1.1.3"
val liquiBaseVersion = "2.2.1"
id("org.springframework.boot") version springBootVersion
id("io.spring.dependency-management") version springDependencyManagementVersion
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version kotlinVersion
id("org.liquibase.gradle") version liquiBaseVersion
}
// Make liquibase extend from main runtime, letting it be able to see/interact with hibernate
configurations {
liquibaseRuntime.extendsFrom(runtimeClasspath)
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
runtimeOnly("org.postgresql:postgresql")
implementation("io.hypersistence:hypersistence-utils-hibernate-62:${hipersistenceUtilsVersion}")
testRuntimeOnly("com.h2database:h2")
liquibaseRuntime("org.liquibase:liquibase-core:${liquibaseVersion}")
liquibaseRuntime("org.postgresql:postgresql")
liquibaseRuntime("org.liquibase.ext:liquibase-hibernate6:${liquibaseVersion}")
liquibaseRuntime("info.picocli:picocli:${picocliVersion}")
// To connect with hibernate liquibase needs to be able to get the main souce sets output
liquibaseRuntime(sourceSets.getByName("main").output)
}
This block will import all the dependencies and setup the config needed for liquibase to be able to access hibernate. Just to note; I've left a lot of other dependencies and plugins that aren't relevant.
This next part is the main part of the config so I will go through it in smaller bits:
// Loading in liquibase configuration properties
val liquibasePropertyFile = file("src/main/resources/db/liquibase.properties")
val loadLiquibaseProperties = Properties()
if (liquibasePropertyFile.exists()) {
loadLiquibaseProperties.load(liquibasePropertyFile.inputStream())
}
val liquibaseProperties = loadLiquibaseProperties.toMutableMap()
// Otherwise use env var if exists
// Otherwise default to liquibase.properties
System.getenv("LIQUIBASE_CHANGELOG")?.let { liquibaseProperties["changelogFile"] = it }
System.getenv("LIQUIBASE_REFERENCE_URL")?.let { liquibaseProperties["reference-url"] = it }
System.getenv("DB_USERNAME")?.let { liquibaseProperties["username"] = it }
System.getenv("DB_PASSWORD")?.let { liquibaseProperties["password"] = it }
System.getenv("DB_SCHEMA")?.let { liquibaseProperties["defaultSchemaName"] = it }
// Create database url
val liquibaseHost: String? = System.getenv("DB_HOST")
val liquibasePort: String? = System.getenv("DB_PORT")
// If DB_HOST and DB_PORT env vars are set, assume database name is 'exampledatabase'
val liquibaseDbName = System.getenv("DB_NAME") ?: "exampledatabase"
if (liquibaseHost != null && liquibasePort != null) {
liquibaseProperties["url"] = "jdbc:postgresql://$liquibaseHost:$liquibasePort/$liquibaseDbName"
}
This block enables me to set the liquibase configuration in the liquibase.properties file and override all of the values with env vars for CI/CD and different environments.
My liquibase.properties
file looks like something along the lines of this:
[!IMPORTANT] Even though this is a
liquibase.properties
it does not follow the standard liquibase property naming schema. Rather, it follows this document, where it is basically the same but mostly? camelCase instead of kebab-case.
changelogFile=src/main/resources/db/changelog-master.yaml
username=demo
password=123456
url=jdbc:postgresql://localhost:5432/example
defaultSchemaName=public
reference-url=hibernate:spring:au.com.example.package.entity?dialect=org.hibernate.dialect.PostgreSQLDialect
logLevel=info
The hibernate reference url doco is here
This will create all of the config needed to be able to register a basic liquibase task, however, if you want to be able to generate/diff changelogs then some config needs to be created to generate the changelogs into seperate sub files.
Instead, I have a (very unoptimised) script for checking the default changelog folder for the last changelogs index
tasks.withType(LiquibaseTask::class.java).forEach {
// it.doFirst is used to ensure that unique changelog names are only set when diffChangelog and generateChangelog are run
it.doFirst {
// If the task generates a changelog, change the changelog filepath to a new sub path
// and auto generate a unique name
if (it.name == "diffChangelog" || it.name == "generateChangelog") {
val changelogPath = "src/main/resources/db/changelogs/"
val existingChangelogs = Path(changelogPath).listDirectoryEntries("*.changelog.yaml")
var lastChangeLog = 0
existingChangelogs.forEach { path ->
// Get the auto generated preceding number, these define the run order for the changelog
val changelogNum = path.nameWithoutExtension.split(".")[0].toIntOrNull()
// If naming scheme has been broken and no integer exists, ignore the changelog.
// Otherwise, if it is the biggest so far then save it
if (changelogNum != null && changelogNum > lastChangeLog) {
lastChangeLog = changelogNum
}
}
// Enable a description for each file to be set when running this task, eg: `./gradlew generateChangelog -Pdescription=short-changelog-description`
val changelogDescription = when (val desc = properties["description"]) {
null -> ""
else -> ".$desc"
}
// Format the changelog file as 00.<provided description>.changelog.yaml
liquibaseProperties["changelogFile"] = changelogPath +
String.format("%02d", lastChangeLog + 1) +
changelogDescription +
".changelog.yaml"
}
// Finally, register the liquibase activity
liquibase.activities.register("main") { arguments = liquibaseProperties }
}
}
That is all of my config I need for my build file.
The final important bit of configuration is the changelog-master.yaml
file. It is a fairly short file which includes everything in the changelog folder first, then everything in the dataseed folder. This order is important so that the seeding process happens after all the changelogs have been run.
databaseChangeLog:
- includeAll:
path: changelogs/
relativeToChangelogFile: true
endsWithFilter: .changelog.yaml
- includeAll:
path: dataseeds/
relativeToChangelogFile: true
endsWithFilter: .dataseed.yaml
The dataseed files are just liquibase changelogs with loadData
changesets.
This is the extent of my configuration, all of my hibernate entity classes are defined in the au.com.example.package.entity package.
I'd love some feedback on where it could be improved/cleaned up - if there is anything else you were looking for config lmk
Okay, here we go:
Overview
This is my stack
- spring boot
liquibase with these plugins:
- hibernate
- kotlin
Quick note; I haven't setup liquibase kotlin dsl plugin as we already use yaml for a lot of our IaC/other config
File structure
I've only included what I thought is relevant, am happy to update with the rest of the projects structure if it makes things clearer
src main kotlin resources application.yml db liquibase.properties master-changelog.yaml changelogs 00.creating-types.changelog.yaml 01.creating-tables.changelog.yaml dataseeds 00.seeding-users.dataseed.yaml build.gradle.kts .gradle
Build Script
The first part of the config is fairly standard
plugins { val kotlinVersion = "1.9.0" val springBootVersion = "3.1.4" val springDependencyManagementVersion = "1.1.3" val liquiBaseVersion = "2.2.1" id("org.springframework.boot") version springBootVersion id("io.spring.dependency-management") version springDependencyManagementVersion kotlin("jvm") version kotlinVersion kotlin("plugin.spring") version kotlinVersion kotlin("plugin.jpa") version kotlinVersion id("org.liquibase.gradle") version liquiBaseVersion } // Make liquibase extend from main runtime, letting it be able to see/interact with hibernate configurations { liquibaseRuntime.extendsFrom(runtimeClasspath) } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-security") runtimeOnly("org.postgresql:postgresql") implementation("io.hypersistence:hypersistence-utils-hibernate-62:${hipersistenceUtilsVersion}") testRuntimeOnly("com.h2database:h2") liquibaseRuntime("org.liquibase:liquibase-core:${liquibaseVersion}") liquibaseRuntime("org.postgresql:postgresql") liquibaseRuntime("org.liquibase.ext:liquibase-hibernate6:${liquibaseVersion}") liquibaseRuntime("info.picocli:picocli:${picocliVersion}") // To connect with hibernate liquibase needs to be able to get the main souce sets output liquibaseRuntime(sourceSets.getByName("main").output) }
This block will import all the dependencies and setup the config needed for liquibase to be able to access hibernate. Just to note; I've left a lot of other dependencies and plugins that aren't relevant.
This next part is the main part of the config so I will go through it in smaller bits:
// Loading in liquibase configuration properties val liquibasePropertyFile = file("src/main/resources/db/liquibase.properties") val loadLiquibaseProperties = Properties() if (liquibasePropertyFile.exists()) { loadLiquibaseProperties.load(liquibasePropertyFile.inputStream()) } val liquibaseProperties = loadLiquibaseProperties.toMutableMap() // Otherwise use env var if exists // Otherwise default to liquibase.properties System.getenv("LIQUIBASE_CHANGELOG")?.let { liquibaseProperties["changelogFile"] = it } System.getenv("LIQUIBASE_REFERENCE_URL")?.let { liquibaseProperties["reference-url"] = it } System.getenv("DB_USERNAME")?.let { liquibaseProperties["username"] = it } System.getenv("DB_PASSWORD")?.let { liquibaseProperties["password"] = it } System.getenv("DB_SCHEMA")?.let { liquibaseProperties["defaultSchemaName"] = it } // Create database url val liquibaseHost: String? = System.getenv("DB_HOST") val liquibasePort: String? = System.getenv("DB_PORT") // If DB_HOST and DB_PORT env vars are set, assume database name is 'exampledatabase' val liquibaseDbName = System.getenv("DB_NAME") ?: "exampledatabase" if (liquibaseHost != null && liquibasePort != null) { liquibaseProperties["url"] = "jdbc:postgresql://$liquibaseHost:$liquibasePort/$liquibaseDbName" }
This block enables me to set the liquibase configuration in the liquibase.properties file and override all of the values with env vars for CI/CD and different environments.
My
liquibase.properties
file looks like something along the lines of this:Important
Even though this is a
liquibase.properties
it does not follow the standard liquibase property naming schema. Rather, it follows this document, where it is basically the same but mostly? camelCase instead of kebab-case.changelogFile=src/main/resources/db/changelog-master.yaml username=demo password=123456 url=jdbc:postgresql://localhost:5432/example defaultSchemaName=public reference-url=hibernate:spring:au.com.example.package.entity?dialect=org.hibernate.dialect.PostgreSQLDialect logLevel=info
The hibernate reference url doco is here
This will create all of the config needed to be able to register a basic liquibase task, however, if you want to be able to generate/diff changelogs then some config needs to be created to generate the changelogs into seperate sub files.
Instead, I have a (very unoptimised) script for checking the default changelog folder for the last changelogs index
tasks.withType(LiquibaseTask::class.java).forEach { // it.doFirst is used to ensure that unique changelog names are only set when diffChangelog and generateChangelog are run it.doFirst { // If the task generates a changelog, change the changelog filepath to a new sub path // and auto generate a unique name if (it.name == "diffChangelog" || it.name == "generateChangelog") { val changelogPath = "src/main/resources/db/changelogs/" val existingChangelogs = Path(changelogPath).listDirectoryEntries("*.changelog.yaml") var lastChangeLog = 0 existingChangelogs.forEach { path -> // Get the auto generated preceding number, these define the run order for the changelog val changelogNum = path.nameWithoutExtension.split(".")[0].toIntOrNull() // If naming scheme has been broken and no integer exists, ignore the changelog. // Otherwise, if it is the biggest so far then save it if (changelogNum != null && changelogNum > lastChangeLog) { lastChangeLog = changelogNum } } // Enable a description for each file to be set when running this task, eg: `./gradlew generateChangelog -Pdescription=short-changelog-description` val changelogDescription = when (val desc = properties["description"]) { null -> "" else -> ".$desc" } // Format the changelog file as 00.<provided description>.changelog.yaml liquibaseProperties["changelogFile"] = changelogPath + String.format("%02d", lastChangeLog + 1) + changelogDescription + ".changelog.yaml" } // Finally, register the liquibase activity liquibase.activities.register("main") { arguments = liquibaseProperties } } }
That is all of my config I need for my build file.
Root changelog
The final important bit of configuration is the
changelog-master.yaml
file. It is a fairly short file which includes everything in the changelog folder first, then everything in the dataseed folder. This order is important so that the seeding process happens after all the changelogs have been run.databaseChangeLog: - includeAll: path: changelogs/ relativeToChangelogFile: true endsWithFilter: .changelog.yaml - includeAll: path: dataseeds/ relativeToChangelogFile: true endsWithFilter: .dataseed.yaml
The dataseed files are just liquibase changelogs with
loadData
changesets.This is the extent of my configuration, all of my hibernate entity classes are defined in the au.com.example.package.entity package.
I'd love some feedback on where it could be improved/cleaned up - if there is anything else you were looking for config lmk
I followed your example and other resources and finally got it to almost work...
Now I get:
2024-09-30T14:42:32.714+0200 [DEBUG] [org.gradle.api.Project] skipping the changelogFile command argument because it is not supported by the generateChangelog command 2024-09-30T14:42:32.714+0200 [DEBUG] [org.gradle.api.Project] skipping the logLevel command argument because it is not supported by the generateChangelog command 2024-09-30T14:42:32.714+0200 [DEBUG] [org.gradle.api.Project] skipping the password command argument because it is not supported by the generateChangelog command 2024-09-30T14:42:32.715+0200 [DEBUG] [org.gradle.api.Project] skipping the referenceUrl command argument because it is not supported by the generateChangelog command 2024-09-30T14:42:32.715+0200 [DEBUG] [org.gradle.api.Project] skipping the url command argument because it is not supported by the generateChangelog command 2024-09-30T14:42:32.715+0200 [DEBUG] [org.gradle.api.Project] skipping the username command argument because it is not supported by the generateChangelog command
The arguments are passed in but are considered not supported by the command... What the hell is going on here? Using "org.liquibase.gradle" 3.0.1 and gradle wrapper version 8.8.
I spent several hours researching how to make work this plugin on the stack:
I still face issues with making it work without exceptions. Let's create an up-to-date example as documentation for this conventional dev stack, please.
I'm ready to help.