Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spring boot v3 integration example #134

Open
landsman opened this issue Oct 15, 2023 · 6 comments
Open

spring boot v3 integration example #134

landsman opened this issue Oct 15, 2023 · 6 comments

Comments

@landsman
Copy link

landsman commented Oct 15, 2023

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.

@ZooToby
Copy link

ZooToby commented Jan 11, 2024

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.

@landsman
Copy link
Author

@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.

@ZooToby
Copy link

ZooToby commented Jan 11, 2024

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

@landsman
Copy link
Author

That sounds good. I'd like to take a look at the configuration and provide some feedback.

@ZooToby
Copy link

ZooToby commented Jan 12, 2024

Okay, here we go:

Overview

This is my stack

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

@AlSherif
Copy link

Okay, here we go:

Overview

This is my stack

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants