quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.71k stars 2.66k forks source link

Generate Database Migration from hibernate model #43723

Open rmanibus opened 1 week ago

rmanibus commented 1 week ago

Description

Today, there is no real way to generate db migrations.

Current Process: When creating a database migration, the workflow is quite cumbersome:

New Workflow: the process is simplified to:

This could then be executed either from the dev UI, or by registering a new maven / gradle task, that way it is accessible even if the app does not start.

I remember when I first started coding in PHP, doctrine was doing this very well: https://symfony.com/bundles/DoctrineMigrationsBundle/current/index.html

Implementation ideas

Liquidbase provide a diff command that compute the difference between two database, and a generate changelog command that generate migrations from this diff.

Instead of comparing real databases, we could compare a virtual database created from hibernate persistence model and the local dev db.

~~There is already some work around a hibernate virtual db, but it is not working with quarkus: https://github.com/liquibase/liquibase-hibernate~~

See linked PR

quarkus-bot[bot] commented 1 week ago

/cc @andrejpetras (liquibase), @geoand (liquibase), @gsmet (hibernate-orm,liquibase), @yrodiere (hibernate-orm)

rmanibus commented 1 week ago

The way it has been done in liquidbase-hibernate is by leveraging the org.hibernate.boot.Metadata. There is a way to access the metadata object using the Integrator spi, I did some experiment around that in the attached PR.

siddharthapd commented 1 week ago

@rmanibus Thanks for raising this here, I did created an issue in liquibase-hibernate project https://github.com/liquibase/liquibase-hibernate/issues/626 last year.

Secondly there is a way to integrate liquibase-hibernate which can generate diff. The only catch / hack is that in your gradle goals for liquibaseRuntime you need to import spring dependency for that. i have provided a working example here --> https://github.com/liquibase/liquibase-hibernate/issues/436#issuecomment-1869420287

Please check Issue comment - 1869420287

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    liquibase
    liquibaseRuntime.extendsFrom runtime
}

ext {
    diffChangeLogVersion = "CHANGE-0002"
    rollbackTagVersion = "CHANGE-0002"
    diffChangeLogFile = "src/main/resources/XXXX/db-changelog-${diffChangeLogVersion}.oracle.sql"
    entitiesPackage =  XXX.XXX.XXX.XXX"
    hibernateGenericDialect = "org.hibernate.dialect.OracleDialect"
    springCoreVersion = "6.1.2"
    springDataVersion = "3.2.1"
}

dependencies {
    // Liquibase
    implementation "io.quarkus:quarkus-liquibase"
    implementation "org.liquibase:liquibase-core:4.25.1"
    liquibaseRuntime "org.liquibase:liquibase-core:4.25.1"
    liquibaseRuntime "org.liquibase.ext:liquibase-hibernate6:4.25.1"
    //liquibaseRuntime "org.liquibase:liquibase-groovy-dsl:3.0.2"
    liquibaseRuntime "info.picocli:picocli:4.7.5"
    liquibaseRuntime "com.oracle.database.jdbc:ojdbc11-production:23.2.0.0"
    liquibaseRuntime "javax.xml.bind:jaxb-api:2.3.1"
    liquibaseRuntime "ch.qos.logback:logback-core:1.2.9"
    liquibaseRuntime "ch.qos.logback:logback-classic:1.2.9"

    liquibaseRuntime "org.springframework:spring-core:${springCoreVersion}"
    liquibaseRuntime "org.springframework.data:spring-data-jpa:${springDataVersion}"
    liquibaseRuntime "org.springframework.data:spring-data-envers:${springDataVersion}"
    liquibaseRuntime sourceSets.main.output
}

task deleteDiffChangeLog(type: Delete) {
    delete diffChangeLogFile
}

task liquibaseEntitiesToDbDiffChangelog(type: JavaExec) {
    dependsOn deleteDiffChangeLog
    group = "liquibase"
    classpath sourceSets.main.runtimeClasspath
    classpath configurations.liquibaseRuntime
    mainClass = "liquibase.integration.commandline.LiquibaseCommandLine"
    args "--logLevel=FINE"
    args "--changeLogFile=${diffChangeLogFile}"
    args "--url=${dbURL}"
    args "--username=${dbUser}"
    args "--password=${dbPassword}"
    args "--defaultSchemaName=${dbSchema}"
    args "--driver=${dbDriver}"
    args "--referenceUrl=hibernate:spring:${entitiesPackage}?dialect=${hibernateGenericDialect}"
    args "diffChangeLog"
}
siddharthapd commented 1 week ago

Also there is a problem with current liquibase-hibernate plugin version 4.29.2 i.e it does not generate changelog against empty local database, which is kind of absurd IMHO. we definately need something quarkus native diff plugin to generate the diff. current liquibase-hibernate plugin is very much broken with latest hibernate versions.

yrodiere commented 1 week ago

If you want the migration script to be generated automatically, why not use the Hibernate feature?

For context, Hibernate ORM is able to create initial DDL scripts, and we already have something that allows turning that into the initial DDL script for Flyway, using a button in the Dev UI:

https://github.com/quarkusio/quarkus/blob/f308816d5a71ce41554f1b9476e0872548372819/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devui/FlywayJsonRpcService.java#L97-L111

https://github.com/quarkusio/quarkus/blob/63edc9f9aa4e89804229688979186ff03f5f888f/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/dev/HibernateOrmDevUIProcessor.java#L68

We could most likely do that for Liquibase as well.

Back to the point, Hibernate ORM is also able to create "update" DDL, to migrate your schema from what it currently is to what Hibernate ORM expects. So... we could have something similar, another button on the Dev UI to add a Flyway/Liquibase migration?

And that wouldn't require thousands of lines of code like in #43724 .

EDIT: Note that I'm not personally recommending such automatic DDL updates, as they can miss critical details and lead to loss of data (e.g. if you rename a column). But I guess if a human has a look at the script after it's generated and before it's executed, that could work.

rmanibus commented 1 week ago

I didn't know that hibernate was able to create "diff" scripts, that's a super interesting lead !

@yrodiere to your point, the goal here is not to let hibernate blindly manage migrations, but rather to create a base to simplify developer's life

rmanibus commented 1 week ago

one more consideration: Generally speaking running from the dev UI is mostly fine, but in some cases, the app would not start without the migration applied, in this case it is impossible to trigger the migration from the dev UI, so we should have an alternate way of running it.

rmanibus commented 5 days ago

I will provide a PR for this one

siddharthapd commented 6 hours ago

@rmanibus are you not creating anything for liquibase ?

rmanibus commented 4 hours ago

I think both are fairly easy to implement, I am just going to start by flyway because that's what my project is using.