-
Notifications
You must be signed in to change notification settings - Fork 24
Start Here
We are still in the progress of documenting Morf as a standalone product. It previously existing as part of Alfa Systems, and is still tied to it in some awkward ways. We're working towards finalising what the "official" API looks like. These instructions will help you to get going, but please don't be tempted to integrate it into a production application yet, and wait instead for the API to stabilise.
Maven:
<dependency>
<groupId>org.alfasoftware</groupId>
<artifactId>morf-core</artifactId>
<version>0.1.3</version>
</dependency>
Plus H2 database support. Our tutorial will use the H2 in-memory database for convenience:
<dependency>
<groupId>org.alfasoftware</groupId>
<artifactId>morf-h2</artifactId>
<version>0.1.3</version>
</dependency>
In this basic tutorial, we'll be talking through the code example in TestStartHere
. If you prefer to learn by reading code, just check out Morf locally and read/run that example test.
First, we need to tell Morf what your target database Schema
should look like. This is used for several things:
- Initial deployment. It is possible to integrate Morf with an existing application with an existing database structure, and Morf will recreate that initial schema on startup. You don't need to create an "init" upgrade step which creates that position.
- Forward verification. Verifying that new/unprocessed upgrade steps, when applied to the current schema, will produce the target schema. Morf checks your unprocessed upgrade steps and confirms that they will result in the target schema before doing anything. This is a powerful protection against broken upgrade steps.
- Reverse verification. Verifying that reverse applying the new/unprocessed upgrades steps to the target schema results in the current schema. This is a belt-and-braces additional check which can show up some specific types of upgrade step problem.
To define the schema, add the following:
import static org.alfasoftware.morf.metadata.DataType.BIG_INTEGER;
import static org.alfasoftware.morf.metadata.DataType.STRING;
import static org.alfasoftware.morf.metadata.SchemaUtils.column;
import static org.alfasoftware.morf.metadata.SchemaUtils.schema;
import static org.alfasoftware.morf.metadata.SchemaUtils.table;
import static org.alfasoftware.morf.upgrade.db.DatabaseUpgradeTableContribution.deployedViewsTable;
import static org.alfasoftware.morf.upgrade.db.DatabaseUpgradeTableContribution.upgradeAuditTable;
...
// Define the target database schema
Schema targetSchema = schema(
deployedViewsTable(),
upgradeAuditTable(),
table("Test1").columns(
column("id", BIG_INTEGER).autoNumbered(1),
column("val", STRING, 100)
),
table("Test2").columns(
column("id", BIG_INTEGER).autoNumbered(1),
column("val", STRING, 100)
)
);
This defines two tables called Test1
and Test2
, plus two essential tables which are used by Morf itself: UpgradeAudit
and DeployedViews
. Note that this doesn't actually do anything; we're just defining the schema here, not deploying it.
Usually you would define this schema somewhere centrally in your application, perhaps as a singleton. If you have a large number of developers, you can avoid conflicts and maintain good encapsulation using tools such as Guice multibindings to contribute sets of
Table
andView
, then constructing theSchema
at run time. For now, though, we'll keep things simple.
An UpgradeStep
in Morf is equivalent to a "migration" in some similar tools.
We're going to start with an empty list of UpgradeStep
s, and rely on Morf's Deployment
feature to initialise the database.
// No initial upgrades
Collection<Class<? extends UpgradeStep>> upgradeSteps = new HashSet<>();
As with your
Schema
, in practice you would most likely declare this in a central location, and add newUpgradeStep
s each time you wish to add a migration. Again, Guice multibindings are a great way of contributing these across your application without maintaining a monolithic single class which is a constant source of merge conflicts.
The simplest way to specify connection details is using a ConnectionResourcesBean
. We will use an in-memory H2 database to avoid the need to set up a fully fledged RDBMS for our example:
ConnectionResourcesBean connectionResources = new ConnectionResourcesBean();
connectionResources.setDatabaseType("H2");
connectionResources.setHostName("localhost");
connectionResources.setDatabaseName("test");
connectionResources.setUserName("test");
connectionResources.setPassword("test");
There are various other ways of doing this, but as a rule you will want these settings to come from run-time configuration.
It's time to actually do something! To connect to the empty in-memory DB and deploy the initial schema, add the following:
// Deploy the schema
Deployment.deploySchema(targetSchema, upgradeSteps, connectionResources);
Now add the following to open the H2 inspector so you can see the database schema:
new H2DatabaseInspector(connectionResources).inspect();
Now run the code! You should see the H2 inspector showing that your schema has been deployed. When you exit the inspector, the in-memory database will be destroyed.
In the complete solution (
TestStartHere
) we replace this with some assertions which actually check the database schema.
We're now going to add some more code to our example which will take the Schema
we defined in the previous steps, and upgrade it with a new table and a new view, and also do some data manipulation.
Remember, we're working with an in-memory DB, so append the following to your existing code so it starts with the deployed schema from the previous exercise.
Firstly, we must define a new schema which represents our new target. We do this by extending the previous one:
// Now let's extend the schema, with an extra table and a view
targetSchema = schema(
targetSchema, // Combining this
schema( // And this
table("Test3").columns(
column("id", BIG_INTEGER).autoNumbered(1),
column("val", STRING, 100)
)
),
schema( // And this
view("Test3View", select().from("Test3"))
)
);
A couple of things to note here:
- You don't have to define your next
Schema
by extending the previous one in this way. We're just using Morf'sSchema
manipulation tools for this demonstration. In practice, you only need oneSchema
in your application: the target one. - We have defined both a new
Table
and a newView
. Views are created automatically as require; noUpgradeStep
s are needed. However, we now need to define anUpgradeStep
to create theTest3
table.
Now add the following inner class to define the upgrade:
import static org.alfasoftware.morf.sql.SqlUtils.field;
import static org.alfasoftware.morf.sql.SqlUtils.insert;
import static org.alfasoftware.morf.sql.SqlUtils.literal;
import static org.alfasoftware.morf.sql.SqlUtils.select;
import static org.alfasoftware.morf.sql.SqlUtils.tableRef;
import org.alfasoftware.morf.upgrade.DataEditor;
import org.alfasoftware.morf.upgrade.SchemaEditor;
import org.alfasoftware.morf.upgrade.Sequence;
import org.alfasoftware.morf.upgrade.TableContribution;
import org.alfasoftware.morf.upgrade.UUID;
import org.alfasoftware.morf.upgrade.UpgradeStep;
import org.alfasoftware.morf.upgrade.Version;
...
@Sequence(1496853841)
@UUID("d962f6d0-6bfe-4c9f-847b-6319ad99ba54")
@Version("0.0.1")
public static final class CreateTest3 implements UpgradeStep {
@Override
public String getJiraId() {
return "FOO-1";
}
@Override
public String getDescription() {
return "Create table Test3";
}
@Override
public void execute(SchemaEditor schema, DataEditor data) {
schema.addTable(
table("Test3").columns(
column("id", DataType.BIG_INTEGER).autoNumbered(1),
column("val", DataType.STRING, 100)
)
);
data.executeStatement(
insert()
.into(tableRef("Test1"))
.values(literal("Foo").as("val"))
);
data.executeStatement(
insert()
.into(tableRef("Test3"))
.from(
select(
field("id"),
field("val"))
.from("Test1")
)
);
}
}
Add your upgrade to the "master" set of upgrades:
// Add the upgrade
upgradeSteps.add(CreateTest3.class);
And finally run the upgrade and view your handiwork:
// Run the upgrade
Upgrade.performUpgrade(targetSchema, upgradeSteps, connectionResources);
Run your code again, and you should get the inspector appearing twice, once with your initial schema and once with the new schema and records in both Test1
and Test3
.
That's the basics!
In our example, we started off on initial deployment of the schema with no upgrade steps at all. This is completely fine to start off with, but when deploying a mature application with an existing chain of upgrade steps, you want to call Deployment
with the existing set, so these can be marked processed on startup.
You can see the end result (along with some assertions to make sure that the code has done the right thing) in TestStartHere
© 2017-2018 Alfa Financial Software