the gorma Plugin
gorma is a goa plugin that makes it possible to describe database models. The gorma code generator uses the model definitions to generate code that automatically creates media types from models. More documentation to come.
Overview
goa is a DSL for the end-user experience of your API. It describes the inputs and outputs in exact detail. gorma
extends this experience to the data storage layer. gorma
adds a new DSL to describe your storage layer and the relationships between your models. Your gorma
specification lives in the same design
folder as your goa DSL. Here’s an example:
package design
import (
"github.com/goadesign/gorma"
. "github.com/goadesign/gorma/dsl"
)
var _ = StorageGroup("CongoStorageGroup", func() {
Description("This is the global storage group")
Store("postgres", gorma.Postgres, func() {
Description("This is the Postgres relational store")
Model("User", func() {
BuildsFrom(func() {
Payload("user", "create")
Payload("user", "update")
})
RendersTo(User)
Description("User Model Description")
HasMany("Reviews", "Review")
HasMany("Proposals", "Proposal")
Field("id", gorma.Integer, func() {
PrimaryKey()
Description("This is the User Model PK field")
})
Field("created_at", gorma.Timestamp, func() {})
Field("updated_at", gorma.Timestamp, func() {})
Field("deleted_at", gorma.NullableTimestamp, func() {})
})
Model("Proposal", func() {
BuildsFrom(func() {
Payload("proposal", "create")
Payload("proposal", "update")
})
RendersTo(Proposal)
Description("Proposal Model")
BelongsTo("User")
HasMany("Reviews", "Review")
Field("id", gorma.Integer, func() {
PrimaryKey()
Description("This is the Payload Model PK field")
})
Field("title", func() {
Alias("proposal_title")
})
Field("created_at", gorma.Timestamp, func() {})
Field("updated_at", gorma.Timestamp, func() {})
Field("deleted_at", gorma.NullableTimestamp, func() {})
})
Model("Review", func() {
BuildsFrom(func() {
Payload("review", "create")
Payload("review", "update")
})
RendersTo(Review)
Description("Review Model")
BelongsTo("User")
BelongsTo("Proposal")
Field("id", gorma.Integer, func() {
PrimaryKey()
Description("This is the Review Model PK field")
})
Field("created_at", gorma.Timestamp, func() {})
Field("updated_at", gorma.Timestamp, func() {})
Field("deleted_at", gorma.NullableTimestamp, func() {})
})
Model("Test", func() {
Description("TestModel")
NoAutomaticIDFields()
Field("created_at", gorma.Timestamp, func() {})
Field("updated_at", gorma.Timestamp, func() {})
Field("deleted_at", gorma.NullableTimestamp, func() {})
})
})
})
Like goa, gorma
’s DSL is really a set of Go functions that builds a configuration behind the scenes. gorma
uses this configuration to generate a database access package in the models
directory of your project. Let’s walk through the DSL in the above example.
Storage Group
The StorageGroup specification is the root of your gorma
configuration. On its own it creates nothing, but it serves as the container for all of the storage specifications for your project. There may be only one StorageGroup defined in your project. As with nearly all the DSL in goa and gorma, you can add a Description("...")
specification to your StorageGroup.
Store
A Store represents a single data store; today that means a relational database. This restriction may change in the future based on contributions, demand and need. The Store has a name and a type:
Store("postgres", gorma.Postgres, ...)
Today these identifiers aren’t used in code generation, but may be used in the future to change the generated code in database specific ways. The name isn’t special–you can use any name you like.
The example Store above has a Description
and several Model
definitions:
Store("postgres", gorma.Postgres, func() {
Description("This is the Postgres relational store")
Model("User", func() { ...
Your Store may contain an arbitrary number of models, and the models you specify aren’t limited to the ones that are specified in your goa DSL.
Model
A Model represents a single struct
or domain object that maps to a database table. The Model DSL is the most flexible DSL in gorma
, and it’s where you’ll do most of your specification. Models generated by gorma
use gorm to access your database. This means that gorma
supports any database driver that gorm supports. All testing in gorma
is currently done in PostgreSQL. gorma
uses no database specific functionality, so it should work equally well across supported databases.
Let’s break down the Model definition and see what’s happening:
Model("User", func() {
BuildsFrom(func() {
Payload("user", "create")
Payload("user", "update")
})
RendersTo(User)
Description("User Model Description")
HasMany("Reviews", "Review")
HasMany("Proposals", "Proposal")
Field("id", gorma.Integer, func() {
PrimaryKey()
Description("This is the User Model PK field")
})
Field("created_at", gorma.Timestamp, func() {})
Field("updated_at", gorma.Timestamp, func() {})
Field("deleted_at", gorma.NullableTimestamp, func() {})
})
The first stanza in the configuration is the BuildsFrom
DSL. This BuildsFrom
tells gorma
that this model will accept data that is input from the payloads that are sent to the “create” and “update” actions of the User resource. This is important because gorma
will automatically generate conversion functions that convert goa’s strongly typed Payload objects into a User model suitable for the Create and Update methods in the data access code.
The next line specifies that the User model RendersTo
the “User” media type in your goa design. Again, gorma
will automatically generate conversion functions for you that convert the database model into strongly typed goa Views. Because of the power of the goa DSL, gorma
is able to generate code to create the full view object graph. If your User media type specifies that it will return goa Links, gorma
will generate the code to create and embed those links in your view. Similarly, if your view specifies that it returns nested models, gorma
will retrieve and populate those models too. Here’s an example of a goa Media Type that specifies embedded objects in it’s definition:
var Proposal = MediaType("application/vnd.proposal+json", func() {
Description("A response to a CFP")
Reference(ProposalPayload)
Attributes(func() {
Attribute("id", Integer, "ID of user")
Attribute("href", String, "API href of user")
Attribute("title", String, "Response title")
Attribute("abstract", String, "Response abstract")
Attribute("detail", String, "Response detail")
Attribute("reviews", CollectionOf(Review), "Reviews")
})
Links(func(){
Link("reviews")
})
View("default", func() {
Attribute("id")
Attribute("href")
Attribute("title")
Attribute("abstract")
Attribute("detail")
Attribute("reviews")
Attribute("links")
})
View("link", func() {
Attribute("id")
Attribute("href")
Attribute("title")
})
})
Of particular note here is the Attribute("reviews", CollectionOf(Review), "Reviews")
line. The goa DSL specifies that this Proposal media type has an attribute called “reviews” that is actually a collection of the Review media type. This attribute is included in the “default” view of the Proposal media type. In order for this to work in gorma
you need to define the relationship between the models in your DSL. Going back to the User model DSL you can see how gorma
does this:
Model("User", func() {
BuildsFrom(func() {
Payload("user", "create")
Payload("user", "update")
})
RendersTo(User)
Description("User Model Description")
HasMany("Reviews", "Review")
HasMany("Proposals", "Proposal")
...
We’ve adopted the Active Record nomenclature for model relationships. gorma
DSL supports HasMany
, HasOne
, BelongsTo
, and ManyToMany
relationships. The DSL specifies the relationships between gorma
models and needs to match the relationships you specify in your goa Media Types. As in the Media Type definition above, where Proposal includes a collection of Review, the Proposal model in gorma
must declare a HasMany
relationship with the Review model.
In this Model definition example, we’ve included the created_at
, updated_at
, and deleted_at
fields, but it’s not necessary to specify them. gorma
will automatically include them unless you specify the NoAutomaticTimestamps()
DSL in your Store or Model definition. Similarly, gorma
will create a primary key field called id
of type Integer (however that maps to your database’s storage model). You can suppress this with the NoAutomaticIDFields()
DSL in the Store or Model definition.
Fields
gorma
will attempt to populate the fields of your model based on the fields in your BuildsFrom
DSL. This means that if the payloads you reference contain a set of fields, your gorma
model will get those fields too. Sometimes you need to specify extra information about those fields, like when you want to add a secondary index. Other times, you want fields in your Model that aren’t included in any of your Payloads. The Field
DSL allows you to specify field definitions for your Models.
Here’s an excerpt from a Proposal model definition that specifies several properties on the Proposal Model’s fields:
Model("Proposal", func() {
...
Field("id", gorma.Integer, func() {
PrimaryKey()
Description("This is the Payload Model PK field")
})
Field("title", func() {
Alias("proposal_title")
})
Field("created_at", gorma.Timestamp, func() {})
Field("updated_at", gorma.Timestamp, func() {})
Field("deleted_at", gorma.NullableTimestamp, func() {})
...
})
The “id” field is explicitly set here, but as mentioned above it doesn’t need to be. It also includes the PrimaryKey()
definition in the field’s DSL. If you have compound primary keys, you can specify them by creating multiple field definitions in your Model with the PrimaryKey()
tag.
We’ve also specified an Alias
for the “title” field, which tells gorma
that the title
field in the model will be stored in the proposal_title
column in the database. You can specify an Alias for both Model and Field definitions. If you specify an Alias on a table, you’re changing the name of the table in the database.
Running gorma
Generate your models by using the gen
feature of the goagen
command:
goagen --design=gopath/to/your/project/design/ gen --pkg-path=github.com/goadesign/gorma
Expectations
gorma
makes certain expectations, and you shouldn’t deviate too far from them if you want all the automatic wiring to work well.
- Your Model relationships need to match the implicit relationships you specify in your Media Types. When you specify that a Media Type has an Attribute that is actually another Media Type you’re specifying a relationship. That relationship must be also reflected in your
gorma
DSL. - Naming: Name your Models with
CamelCase
. Name your Fields withsnake_case
.gorma
always tries to do the right things with naming and conversions, but keeping this standard will help with edge cases that aren’t yet tested. gorma
generates your models in a folder calledmodels
. You can override this by appending-- --pkg=otherfolder
when you callgoagen
.
Limitations
- Gorma expects that all of your keys (primary and foreign) will be
int
types. In the future we may support non-integer primary keys. - Gorma is currently tested extensively against PostgreSQL. It should work well against other databases supported by Gorm, but if it doesn’t please file a bug.
- The methods generated by
gorma
are basic case methods for data manipulation by primary key. It’s possible to add lookup functions in the future by other fields in the model. Pull requests gratefully accepted. - Gorm ~ and any ORM, really ~ won’t generate optimal SQL for every use case. If you need to optimize a particular case, see the note under “Other Notes” below about adding additional methods to your Models, and hand-write your SQL.
gorma
exposes the underlying database handle so you can drop down to Go’sdb/sql
package and write anything you want in your custom methods.
Other Notes
gorma
generates your models into themodels
folder of your project’s root. It will overwrite these models every time you usegoagen
, but it will only overwrite files that it creates. You can add extra files to this directory to specify additional functionality for your models as long as you use a file name that won’t conflict withgorma
’s naming convention:model.go
andmodel_helper.go
. We recommend something likemodel_addons.go
to keep your extra code from being overwritten.- Gorm’s update model is a little bit scary. If you are writing custom functions, be cautious about how you send the updates to Gorm.
gorma
accounts for this by using pointers on any field that can be null, and checking each field before adding the field to the update clause in the Update functions that it generates. When you’re writing custom Update methods, don’t send a goa payload directly to theSave()
method in Gorm, or you will overwrite columns that are populated in the database with the fields that aren’t populated in your model. Instead, call theUpdate
method in Gorm and explicitly pass in the fields and values you want to update.