Extension Guides~Additonal Database~Snapshot Generator
Liquibase defines a database-independent metadata model in liquibase.snapshot.DatabaseSnapshot That model contains standardized liquibase.structure.DatabaseObject objects such as liquibase.structure.core.Table.
The snapshot logic used in not just liquibase snapshot
but also liquibase diff
and liquibase diff-changelog
relies on liquibase.snapshot.SnapshotGenerator
implementations to read the database's metadata and create the corresponding DatabaseObject
objects
sequenceDiagram
participant DatabaseSnapshot
participant SnapshotFactory
participant SnapshotGenerator
participant Database
DatabaseSnapshot->>SnapshotFactory: Snapshot the DatabaseObjects
SnapshotFactory->>SnapshotGenerator: Snapshot a particular type
SnapshotGenerator->>Database: Executes Query
SnapshotGenerator-->>DatabaseSnapshot: Returns DatabaseObject
For example, to snapshot a Column SnapshotFactory will go through all the available SnapshotGenerators
that say they support Column
for the given database and use the one with the
highest priority.
Defining database-specific functionality tends to be writing if (database instanceof ExampleDatabase)
blocks
around the non-standard calls. Using "instanceof" means the block will apply to any subclasses of the given database.
For example, if your database is PostgreSQL compatible and therefore your Database class extensions PostgresDatabase
then all the
if (database instanceof PostgresDatabase)
checks will be used for your database as well.
When adding support for a new Database, there will be existing SnapshotGenerator
implementations for all DatabaseObject
types.
It is usually easiest to subclass the existing SnapshotGenerator to leverage the standard logic, but it's not necessary.
The naming pattern tends to be "ObjectType" + "SnapshotGenerator". For example, Column -> ColumnSnapshotGenerator.
Database-specific classes append the database name to the end, like ColumnSnapshotGeneratorOracle
.
Like all Liquibase extensions, your SnapshotGenerator must have an empty constructor.
The getPriority
works a bit different than other extensions in that it's not used to choose the single "best" implementation to use, but rather to choose the order to run SnapshotGenerators in.
Like other getPriority methods, if your implementation does not apply for the given type/database combination, return liquibase.snapshot.PRIORITY_NONE
.
However, Liquibase will use ALL instances that return a priority > 0 to snapshot an object.
Therefore, if your extension is replacing a base SnapshotGenerator you override the replaces()
function rather than returning a higher value from getPriority()
.
The replaces()
function lets you say "use this class in the snapshot logic instead of another one".
A good example implementation for a class that replaces the default ColumnSnapshotGenerator is:
@Override
public int getPriority(Class<? extends DatabaseObject> objectType, Database database) {
if (database instanceof ExampleDatabase) {
return super.getPriority(objectType, database);
} else {
return PRIORITY_NONE;
}
}
@Override
public Class<? extends SnapshotGenerator>[] replaces() {
return new Class[] {
ColumnSnapshotGenerator.class
};
}
which relies on the superclass's "should I be part of the snapshot process for the given type" logic but only for ExampleDatabase while also taking the place of ColumnSnapshotGenerator when it's used.
This is the function which is called by the snapshot system to either lookup the information about the given example
object, OR add additional information to it.
Multiple SnapshotGenerator instances will work together to build up the final snapshot of the object.
The overall flow is:
- The calling code defines the "example" object it wants.
For example, if they want to snapshot a Table with the name "address" they construct a liquibase.structure.core.Table
object and set the name to be "address".
To look up the table in a different schema, they set the schema attribute on Table to the desired schema.
The "example" object will contain the fields set that should be used to identify the actual object to snapshot.
- The snapshot system will collect up all the SnapshotGenerators that say they should participate in the snapshot and call the
snapshot
method ingetPriority()
order.
For example, when snapshotting an example Table, the TableSnapshotGenerator.snapshot()
call will run first which captures the table name, schema, etc.
Then the ColumnSnapshotGenerator.snapshot() will run which snapshots all the columns associated with the table and adds those columns to the Table object.
- After all the SnapshotGenerators have added their information to the Table object, it's returned to the calling code.
There is a liquibase.snapshot.jvm.JdbcSnapshotGenerator convenience base class that is used for all the standard DatabaseObject
SnapshotGenerators which
separates the "create the base object" logic from the "add to an object" logic plus has caching built into it.
The standard SnapshotGenerators tend to have overridable functions for commonly variable portions of the snapshot logic, so you generally shouldn't override snapshot itself but more targeted methods instead.
Like all extensions, your SnapshotGenerator must be registered by adding your class name to META-INF/services/liquibase.snapshot.SnapshotGenerator
package com.example.snapshot;
import com.example.database.ExampleDatabase;
import liquibase.database.Database;
import liquibase.snapshot.CachedRow;
import liquibase.snapshot.SnapshotGenerator;
import liquibase.snapshot.jvm.ColumnSnapshotGenerator;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Column;
public class SnapshotColumnGeneratorExample extends ColumnSnapshotGenerator {
@Override
public int getPriority(Class<? extends DatabaseObject> objectType, Database database) {
if (database instanceof ExampleDatabase) {
return super.getPriority(objectType, database);
} else {
return PRIORITY_NONE;
}
}
@Override
public Class<? extends SnapshotGenerator>[] replaces() {
return new Class[] {
ColumnSnapshotGenerator.class
};
}
@Override
protected Object readDefaultValue(CachedRow columnMetadataResultSet, Column columnInfo, Database database) {
if (columnInfo.getType().getTypeName().equalsIgnoreCase("json")) {
return columnMetadataResultSet.get("default_value_json");
} else {
return super.readDefaultValue(columnMetadataResultSet, columnInfo, database);
}
}
}
Talk to us on the Liquibase Forum, Liquibase Discord, or
clone it at https://github.com/liquibase/liquibase.wiki.git
and create an issue with a patch file.