A compact package for organizing and maintaining your entity database metadata.
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.
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.
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)
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()
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.
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)
}
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!
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 = ?;
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!
Want to lend us a hand? Check out our guidelines for contributing.
We are rocking an Apache 2.0 license for this project.
Please check out our code of conduct to get up to speed how we do things.