Skip to content

Start Here

Graham Crockford edited this page Nov 21, 2018 · 19 revisions

Disclaimer

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.

Get Morf

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>

Tutorial

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.

1. Define your schema

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 and View, then constructing the Schema at run time. For now, though, we'll keep things simple.

2. Define your initial list of upgrade steps.

An UpgradeStep in Morf is equivalent to a "migration" in some similar tools.

We're going to start with an empty list of UpgradeSteps, 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 new UpgradeSteps 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.

3. Specify database connection

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.

4. Deploy the initial schema

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.

5. Add an UpgradeStep

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's Schema manipulation tools for this demonstration. In practice, you only need one Schema in your application: the target one.
  • We have defined both a new Table and a new View. Views are created automatically as require; no UpgradeSteps are needed. However, we now need to define an UpgradeStep to create the Test3 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!

6. Define initial upgrades

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