Skip to content

A compact package for organizing and maintaining your entity database metadata.

License

Notifications You must be signed in to change notification settings

freerware/morph

Repository files navigation

morph

A compact package for organizing and maintaining your entity database metadata.

GoDoc Build Status Coverage Status Release License

What is it?

morph organizes and maintains the necessary metadata to map your entities to and from relational database tables. This is accomplished using the Metadata Mapping pattern popularized by Martin Fowler. With these metadata mappings, your application is empowered to construct SQL queries dynamically using the entities themselves.

Why use it?

With morph, your application reaps several benefits:

  • dynamic construction of queries using entities and their fields.
  • metadata generation using files in several formats, including YAML and JSON.
  • decoupling of code responsible for manufacturing queries from code tasked with SQL generation.

How to use it?

Using morph is super straightforward. You utilize Table and Column to organize metadata for your entities and their associated relational representations. Let's suppose you have a User entity in your application (user):

var idCol morph.Column
idCol.SetName("id")
idCol.SetField(Fields.ID)
idCol.SetFieldType("int")
idCol.SetStrategy("struct_field")
idCol.SetPrimaryKey(true)

var usernameCol morph.Column
usernameCol.SetName("username")
usernameCol.SetField(Fields.Username)
usernameCol.SetFieldType("string")
usernameCol.SetStrategy("struct_field")
usernameCol.SetPrimaryKey(false)

var passwordCol morph.Column
passwordCol.SetName("password")
passwordCol.SetField(Fields.Password)
passwordCol.SetFieldType("string")
passwordCol.SetStrategy("struct_field")
passwordCol.SetPrimaryKey(false)

var userTable morph.Table
userTable.SetName("user")
userTable.SetAlias("U")
userTable.SetType(user)
userTable.AddColumns(idCol, usernameCol, passwordCol)

Loading

Capturing the metadata mappings can be tedious, especially if your application has many entities with corresponding relational representations. Instead of constructing them manually, you can instead load a file that specifies the metadata mapping configuration:

{
  "tables": [
    {
      "typeName": "example.User",
      "name": "user",
      "alias": "U",
      "columns": [
        {
          "name": "id",
          "field": "ID",
          "fieldType": "string",
          "fieldStrategy": "struct_field",
          "primaryKey": true
        },
        {
          "name": "username",
          "field": "Username",
          "fieldType": "string",
          "fieldStrategy": "struct_field",
          "primaryKey": false
        },
        {
          "name": "password",
          "field": "Password",
          "fieldType": "string",
          "fieldStrategy": "struct_field",
          "primaryKey": false
        }
      ]
    }
  ]
}
configuration, err := morph.Load("./metadata.json")
if err != nil {
	panic(err)
}

tables := configuration.AsMetadata()

Custom Loader

At this time, we currently support YAML (.yaml, .yml) and JSON (.json) configuration files. However, if you would like to utilize a different file format, you can construct a type that implements morph.Loader and add the appropriate entries in morph.Loaders. The morph.Load function will leverage morph.Loaders by extracting the file extension using the path provided to it.

Reflection

If defining the metadata within files does not suit your fancy, you can leverage reflection to capture the metadata mappings:

razorcrest := Starship {
    ID: 123,
    Name: "Razorcrest",
    LastServicedAt: time.Now().Add(-3*time.Day),
}

table, err := morph.Reflect(razorcrest)
if err != nil {
    panic(err)
}

If you'd rather use a pointer, that works too:

table, err := morph.Reflect(&razorcrest)
if err != nil {
    panic(err)
}

Options

You can customize the reflection process by providing options to the morph.Reflect function. For example, you can specify the field tag to use when reflecting on the struct:

table, err := morph.Reflect(razorcrest, morph.WithTag("morph"))
if err != nil {
    panic(err)
}

There are many options available, so be sure to check out the morph.ReflectOptions type for more information!

Query Generation

Once you have your metadata mappings, you can use them to construct SQL queries. For example, you can generate a UPDATE query for the Starship entity like so:

razorcrest := Starship {
    ID: 123,
    Name: "Razorcrest",
    LastServicedAt: time.Date(1972, 12, 12, 0, 0, 0, 0, time.UTC),
}

table, err := morph.Reflect(razorcrest)
if err != nil {
    panic(err)
}

query, err := table.UpdateQuery()
if err != nil {
    panic(err)
}

fmt.Println(query) // UPDATE ships SET name = ?, last_serviced_at = ? WHERE id = ?;

Options

You can customize the query generation process by providing options to the UpdateQuery, InsertQuery, and DeleteQuery methods. For example, you can change the placeholder used in the query:

query, err := table.UpdateQuery(morph.WithPlaceholder("$", true))
if err != nil {
    panic(err)
}

fmt.Println(query) // UPDATE ships SET name = $1, last_serviced_at = $2 WHERE id = $3;

There are many options available, so be sure to check out the morph.QueryOptions type for more information!

Contribute

Want to lend us a hand? Check out our guidelines for contributing.

License

We are rocking an Apache 2.0 license for this project.

Code of Conduct

Please check out our code of conduct to get up to speed how we do things.

About

A compact package for organizing and maintaining your entity database metadata.

Resources

License

Stars

Watchers

Forks

Packages

No packages published