This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-09-12
Channels
- # ai (1)
- # announcements (7)
- # babashka (32)
- # beginners (23)
- # biff (9)
- # calva (1)
- # cljs-dev (13)
- # clojure (32)
- # clojure-belgium (1)
- # clojure-chicago (15)
- # clojure-europe (24)
- # clojure-india (3)
- # clojure-nl (3)
- # clojure-norway (55)
- # clojure-uk (4)
- # clojurebridge (1)
- # clojurescript (5)
- # core-async (17)
- # data-science (9)
- # datomic (29)
- # events (3)
- # fulcro (16)
- # graalvm-mobile (4)
- # helix (15)
- # hyperfiddle (74)
- # introduce-yourself (1)
- # jobs (4)
- # kaocha (12)
- # leiningen (27)
- # lsp (16)
- # shadow-cljs (6)
- # spacemacs (20)
- # sql (27)
- # squint (7)
- # tools-deps (29)
- # vim (2)
- # xtdb (10)
Is anybody using liquibase for their db migrations?
Do you also use custom migrations written in Clojure?
If so, how do you handle them - do you use gen-class
and AOT-compile your migrations, even in the dev mode?
That's our current approach but it's cumbersome (especially when I'm porting leiningen-based project to deps.edn).
I tried to use deftype
instead but it throws ClassNotFoundException
More details in the thread.
(deftype CustomTaskChange []
liquibase.change.custom.CustomTaskChange
(getConfirmationMessage [this] "something")
(setFileOpener [this resourceAccessor] nil)
(setUp [this] nil)
(^ValidationErrors validate [this ^Database database] (ValidationErrors.))
(^void execute [this ^Database database] (println "Dummy transaction execution...."))
liquibase.change.custom.CustomTaskRollback (^void rollback [this ^Database database] nil))
;; => myapp.database.migrations.mig2808.CustomTaskChange
;; defining the migration in an XML file like this:
<databaseChangeLog
xmlns=""
xmlns:xsi=""
xsi:schemaLocation=" ">
<changeSet id="2080_01">
<customChange class="myapp.database.migrations.Mig2808" />
</changeSet>
</databaseChangeLog>
;; then triggering the migrations
1. Caused by java.lang.ClassNotFoundException
myapp.database.migrations.Mig2808.CustomTaskChange
URLClassLoader.java: 445 java.net.URLClassLoader/findClass
DynamicClassLoader.java: 69 clojure.lang.DynamicClassLoader/findClass
ClassLoader.java: 592 java.lang.ClassLoader/loadClass
DynamicClassLoader.java: 77 clojure.lang.DynamicClassLoader/loadClass
ClassLoader.java: 525 java.lang.ClassLoader/loadClass
Class.java: -2 java.lang.Class/forName0
Class.java: 467 java.lang.Class/forName
CustomChangeWrapper.java: 79 liquibase.change.custom.CustomChangeWrapper/setClass
CustomChangeWrapper.java: 298 liquibase.change.custom.CustomChangeWrapper/load
ChangeSet.java: 535 liquibase.changelog.ChangeSet/toChange
ChangeSet.java: 460 liquibase.changelog.ChangeSet/handleChildNode
ChangeSet.java: 384 liquibase.changelog.ChangeSet/load
DatabaseChangeLog.java: 761 liquibase.changelog.DatabaseChangeLog/createChangeSet
...
I don't know how much you've edited the copy'n'paste above but these don't match:
cacsremoteservice.database.migrations.mig2808.CustomTaskChange
<customChange class="myapp.database.migrations.Mig2808" />
1. Caused by java.lang.ClassNotFoundException
myapp.database.migrations.Mig2808.CustomTaskChange
https://github.com/metabase/metabase/blob/master/src/metabase/db/custom_migrations.clj
@U04V70XH6 I wasn't diligent enough when editing the example. But in real life, those names do match.
@U11BV7MTK thanks for the link!
I don't fully understand the implementation but it seems that it's simply using defrecord
, similarly to how I used deftype
.
Are there any more tricks to make it work with Liquibase classloading?
So I tried to copy those metabase macros to my project:
(ns cacsremoteservice.database.migrations.custom-migrations
"Most of this code was copied from metabase: "
(:require
[clojure.java.jdbc :as jdbc]
[taoensso.timbre :as log]))
(defmacro define-reversible-migration
"Define a reversible custom migration. Both the forward and reverse migrations are defined using the same structure,
similar to the bodies of multi-arity Clojure functions.
Example:
clj
(define-reversible-migration ExampleMigrationName tx
(migration-body tx)
(reverse-migration-body tx)))
"
[name tx-symbol migration-body reverse-migration-body]
`(defrecord ~name []
liquibase.change.custom.CustomTaskChange
(execute [_# database#]
(jdbc/with-db-transaction [~tx-symbol {:connection (.getUnderlyingConnection (.getConnection database#))}]
~migration-body))
(getConfirmationMessage [_#]
(str "Custom migration: " ~name))
(setUp [_#])
(validate [_# _database#]
(liquibase.exception.ValidationErrors.))
(setFileOpener [_# _resourceAccessor#])
liquibase.change.custom.CustomTaskRollback
(rollback [_# database#]
(jdbc/with-db-transaction [~tx-symbol {:connection (.getUnderlyingConnection (.getConnection database#))}]
~reverse-migration-body))))
(defn no-op
"No-op logging rollback function"
[n]
(log/info "No rollback for: " n))
(defmacro define-migration
"Define a custom migration without a reverse migration."
[name tx-symbol & migration-body]
`(define-reversible-migration ~name ~tx-symbol (do ~@migration-body) (no-op ~(str name))))
Then use use it to define those migrations in seperate namespaces, e.g.
(ns cacsremoteservice.database.migrations.mig1122-01
(:require
[cacsremoteservice.database.migrations.custom-migrations :as custom-migrations]))
(defn execute [db-spec]
...)
(custom-migrations/define-migration MyFirstMigration tx
(execute tx))
;; => cacsremoteservice.database.migrations.mig1122_01.MyFirstMigration
And reference it in the xml file
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog
xmlns=""
xmlns:xsi=""
xsi:schemaLocation=" ">
<changeSet id="1122_01">
<customChange class="cacsremoteservice.database.migrations.Mig1122_01.MyFirstMigration" />
</changeSet>
</databaseChangeLog>
But that is still failing with ClassNotFoundException
as before
1. Caused by java.lang.ClassNotFoundException
cacsremoteservice.database.migrations.Mig1122_01.MyFirstMigration
URLClassLoader.java: 445 java.net.URLClassLoader/findClass
DynamicClassLoader.java: 69 clojure.lang.DynamicClassLoader/findClass
ClassLoader.java: 592 java.lang.ClassLoader/loadClass
DynamicClassLoader.java: 77 clojure.lang.DynamicClassLoader/loadClass
ClassLoader.java: 525 java.lang.ClassLoader/loadClass
Class.java: -2 java.lang.Class/forName0
Class.java: 467 java.lang.Class/forName
CustomChangeWrapper.java: 79 liquibase.change.custom.CustomChangeWrapper/setClass
CustomChangeWrapper.java: 298 liquibase.change.custom.CustomChangeWrapper/load
ChangeSet.java: 535 liquibase.changelog.ChangeSet/toChange
ChangeSet.java: 460 liquibase.changelog.ChangeSet/handleChildNode
ChangeSet.java: 384 liquibase.changelog.ChangeSet/load
DatabaseChangeLog.java: 761 liquibase.changelog.DatabaseChangeLog/createChangeSet
i think that should be solved by these lines
(defn- load-class ^Class [^String class-name]
(Class/forName class-name true (classloader/the-classloader)))
(defrecord ^:private ClassLoadHelper []
org.quartz.spi.ClassLoadHelper
(initialize [_])
(getClassLoader [_]
(classloader/the-classloader))
(loadClass [_ class-name]
(load-class class-name))
(loadClass [_ class-name _]
(load-class class-name)))
(when-not *compile-files*
(System/setProperty "org.quartz.scheduler.classLoadHelper.class" (.getName ClassLoadHelper)))
this is how those classes are found by the liquibase classloaderhere’s where we first introduced it: https://github.com/metabase/metabase/pull/28175/files . My memory is coming back a bit. We’re running the migrations from our main thread which has the clojure dynamic class loader so it can see these. Are you running your migrations from some other thread without this classloader? And are you sure the namespaces with the custom migrations is loaded before attempting to run the migrations?
Regarding loading - something I'm fixing right now but at the moment, I was experimenting with this in the REPL by first evaluating the migration definition and then re-running migrations - that should make sure that they are loaded first. The migrations should also run in the same/main thread but I'm not sure what liquibase is doing under the hood...
I'm thinking that this might be it: https://github.com/metabase/metabase/blob/77c64754c02eb0854182b96a0c6e6b96fa3b6b2c/src/metabase/db/liquibase.clj#L63 We construct the Liquibase object just like this
(Liquibase. "liquibase/migrations/master.xml" (ClassLoaderResourceAccessor.) jdbc-connection)
But even changing that to this doesn't help
(Liquibase. "liquibase/migrations/master.xml" (ClassLoaderResourceAccessor. (.getContextClassLoader (Thread/currentThread))) jdbc-connection)
Ah, ok. So that does help. I forgot to update the classname in the xml file after I moved stuff around.
Now I need to sort out tricky cyclic dependencies between those migrations and other db code 😮 But that's another thing. Thanks @U11BV7MTK for the help!
deftype is probably more correct. i don’t think there was any reason for defrecord. probably just for printing at the repl and i was lazy
I've managed to run this successufully in the REPL. However, when I try to run tests (`lein test` or via deps.edn alias and eftest) I'm again getting ClassNotFoundException-s
liquibase.exception.ChangeLogParseException: liquibase.parser.core.ParsedNodeException: liquibase.exception.CustomChangeException: java.lang.ClassNotFoundException: cacsremoteservice.database.migrations.custom_migrations.Mig1122_01
liquibase.parser.core.ParsedNodeException: liquibase.exception.CustomChangeException: java.lang.ClassNotFoundException: cacsremoteservice.database.migrations.custom_migrations.Mig1122_01
liquibase.exception.CustomChangeException: java.lang.ClassNotFoundException: cacsremoteservice.database.migrations.custom_migrations.Mig1122_01
java.lang.ClassNotFoundException: cacsremoteservice.database.migrations.custom_migrations.Mig1122_01
jdk.internal.loader.BuiltinClassLoader.loadClass BuiltinClassLoader.java: 641
jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass ClassLoaders.java: 188
java.lang.ClassLoader.loadClass ClassLoader.java: 525
java.lang.Class.forName0 Class.java
java.lang.Class.forName Class.java: 467
liquibase.change.custom.CustomChangeWrapper.setClass CustomChangeWrapper.java: 79
liquibase.change.custom.CustomChangeWrapper.load CustomChangeWrapper.java: 298
liquibase.changelog.ChangeSet.toChange ChangeSet.java: 535
...
liquibase.Liquibase.update Liquibase.java: 207
cacsremoteservice.database.migrations/migrate/fn migrations.clj: 34
clojure.java.jdbc/db-transaction* jdbc.clj: 807
clojure.java.jdbc/db-transaction* jdbc.clj: 852
clojure.java.jdbc/db-transaction* jdbc.clj: 789
cacsremoteservice.database.migrations/migrate migrations.clj: 19
...
I do require my custom migrations transitively in the db-fixture
namespace.
(:require
:reload-all
:verbose
[cacsremoteservice.database.db :as db]
[cacsremoteservice.database.migrations :as db-migrations]
...
My suggestion would be to find where you call the migration code. And right before that call class on the deftype of your custom migration. Then assert that classforname can find it. And then call your migration code
I tried your suggestion and it all looked well. Until I started logging the classloader too
;; in the REPL
current classloader: #object[clojure.lang.DynamicClassLoader 0xd8d9f44 "clojure.lang.DynamicClassLoader@d8d9f44"]
;; when run via `lein test`
current classloader: #object[jdk.internal.loader.ClassLoaders$AppClassLoader 0x531d72ca "jdk.internal.loader.ClassLoaders$AppClassLoader@531d72ca"]
So it's AppClassLoader
which doesn't know about classes loaded by Clojure's DynamicClassLoader.
I set up Liquibase using (.getContextClassLoader (Thread/currentThread)
(Liquibase. "liquibase/migrations/master.xml"
;; - inspired by
(ClassLoaderResourceAccessor. (.getContextClassLoader (Thread/currentThread))) jdbc-connection)
I found a related discussion here: https://groups.google.com/g/clojure/c/PdVEeU1NbAQ
I cannot quite understand all the details but I guess this is the reason:
> When you aot compile you generate a class file on disk that is visible to the system classloader.
> • When the reference to the Foo class is compiled, it is compiled as a call something like clojure.lang.RT.classForName("Foo");
> ◦ RT.classForName uses different class loaders depend on this that an the other, and in this case it is using the system classloader (the AppClassLoader) when using lein trampoline.
> • When you loaded your clojure code it ran through the compiler which generated bytecode which is loaded via clojure's DynamicClassLoader.
> • The way RT.classForName determines which classloader to use is something like:
> ◦ if there is a DynamicClassloader available from the compiler, use it, otherwise use the system classloader.
> ◦ Once you finish loading code the DynamicClassloader the compiler uses is, uh, for lack of a better word, popped, so it isn't available to RT.classForName.
> • So given that and the error we can deduce that lein trampoline
runs in t[w]o phases: a code loading phase, and then an execution phase.
> ◦ Which is a perfectly reasonable thing to do and easy to do with the command line options to clojure.main.
> • I suspect adding an import of the Foo class would work around this issue, but I strongly recommend you consider not aot compiling instead.
> ◦ AOT compilation is a source of weird behavior and just isn't worth it.
The difference in my case is that I'm actually trying to avoid AOT compilation!
With AOT, all works, it seems because that the on-disk class file becomes available to the AppClassLoader.
The key thing anyway, is that when the tests execute via lein test
there's AppClassLoader instance in use which doesn't know anything about my deftype
-based classes.
I tried to capture the classloader of one of the migration classes and pass that to Liquibase, but it doesn't seem to work. For some reason, it's still seems to be using AppClassLoader
(log/info "current thread classloader: " (.getContextClassLoader (Thread/currentThread)))
(log/info "Clojure's classloader" (.getClassLoader (class custom-migrations/->Mig1122_01)))
(let [jdbc-connection (JdbcConnection. (:connection connection))
liquibase (Liquibase. "liquibase/migrations/master.xml"
(ClassLoaderResourceAccessor. (.getClassLoader (class custom-migrations/->Mig1122_01))) jdbc-connection)]
lein test
...
current thread classloader: #object[jdk.internal.loader.ClassLoaders$AppClassLoader 0x531d72ca "jdk.internal.loader.ClassLoaders$AppClassLoader@531d72ca"]
Clojure's classloader #object[clojure.lang.DynamicClassLoader 0x4bdd094b "clojure.lang.DynamicClassLoader@4bdd094b"]
...
Caused by: java.lang.ClassNotFoundException: ...custom_migrations.Mig1122_01
at jdk.internal.loader.BuiltinClassLoader.loadClass (BuiltinClassLoader.java:641)
jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass (ClassLoaders.java:188)
java.lang.ClassLoader.loadClass (ClassLoader.java:525)
java.lang.Class.forName0 (Class.java:-2)
java.lang.Class.forName (Class.java:467)
liquibase.change.custom.CustomChangeWrapper.setClass (CustomChangeWrapper.java:79)
liquibase.change.custom.CustomChangeWrapper.load (CustomChangeWrapper.java:298)
...
liquibase.Liquibase.update (Liquibase.java:207)
...migrations$migrate$fn__79059.invoke (migrations.clj:35)
I was able to finally solve this problem! Nothing worked except using setting Thread's context class loader before calling liquibase
(let
...
_ (.setContextClassLoader (Thread/currentThread) clojure-classloader)
liquibase (Liquibase. "liquibase/migrations/master.xml"
;; ideally passing clojure-classloader here would be enough, but it doesn't work (see comment above)
(ClassLoaderResourceAccessor. clojure-classloader) jdbc-connection)]
(try
(.update liquibase (Contexts.))
(finally
;; restore the original classloader
(.setContextClassLoader (Thread/currentThread) original-classloader))))))
I was able to finally solve this problem! Nothing worked except using setting Thread's context class loader before calling liquibase
(let
...
_ (.setContextClassLoader (Thread/currentThread) clojure-classloader)
liquibase (Liquibase. "liquibase/migrations/master.xml"
;; ideally passing clojure-classloader here would be enough, but it doesn't work (see comment above)
(ClassLoaderResourceAccessor. clojure-classloader) jdbc-connection)]
(try
(.update liquibase (Contexts.))
(finally
;; restore the original classloader
(.setContextClassLoader (Thread/currentThread) original-classloader))))))