
Security News
Vite Releases Technical Preview of Rolldown-Vite, a Rust-Based Bundler
Vite releases Rolldown-Vite, a Rust-based bundler preview offering faster builds and lower memory usage as a drop-in replacement for Vite.
github.com/bradleypeabody/tmeta
WidgetFactory
struct corresponds to the "widget_factory" table (names configurable, of course).The tmeta
package understands your types and tmetadbr
provides query building using dbr.
To get set up and concisely run your queries:
type Widget struct {
WidgetID string `db:"widget_id" tmeta:"pk"`
Name string `db:"name"`
}
// register things
meta := tmeta.NewMeta()
err := meta.Parse(Widget{})
// database connection with dbr
conn, err := dbr.Open(...)
// use a Builder to construct queries
b := tmetadbr.New(conn, meta)
// don't get scared by the "Must" in front of these functions, it applies to the
// query building steps, not to the queries themselves
// create
widgetID := gouuidv6.NewB64().String() // use what you want for an ID, my personal favorite: github.com/bradleypeabody/gouuidv6
_, err = b.MustInsert(&Widget{WidgetID: widgetID,Name: "Widget A"}).Exec()
// read multiple
var widgetList []Widget
// notice you can modify the queries before they are executed - where, limit, order by, etc.
_, err = b.MustSelect(&widgetList).Where("name LIKE ?", `Widget%`).Load(&widgetList)
// read one
var widget Widget
err = b.MustSelectByID(&widget, widgetID).LoadOne(&widget)
// update
widget.Name = "Widget A1"
_, err = b.MustUpdateByID(&widget).Exec()
// delete
_, err = b.MustDeleteByID(Widget{}, widgetID).Exec()
Variations of these methods without Must
are available if you want type errors to be returned instead of panicing.
With tmetadbr
you can also easily generate the SQL to load related records.
A "has many" relation is used by one table where a second one has an ID that points back to it.
type Author struct {
AuthorID string `db:"author_id" tmeta:"pk"`
NomDePlume string `db:"nom_de_plume"`
BookList []Book `db:"-" tmeta:"has_many"`
}
type Book struct {
BookID string `db:"book_id" tmeta:"pk"`
AuthorID string `db:"author_id"`
Title string `db:"title"`
}
authorT := b.For(Author{})
_, err = b.MustSelectRelation(&author, "book_list").
Load(authorT.RelationTargetPtr(&author, "book_list"))
// or you can load directly into the field, less dynamic but shorter and more clear
_, err = b.MustSelectRelation(&author, "book_list").Load(&author.BookList)
A "has_one" relation is like a "has_many" but is used for a one-to-one relation.
type Category struct {
CategoryID string `db:"category_id" tmeta:"pk"`
Name string `db:"name"`
CategoryInfo *CategoryInfo `db:"-" tmeta:"has_one"`
}
type CategoryInfo struct {
CategoryInfoID string `db:"category_info_id" tmeta:"pk"`
CategoryID string `db:"category_id"`
}
categoryT := b.For(Category{})
err = b.MustSelectRelation(&category, "category_info").
LoadOne(categoryT.RelationTargetPtr(&category, "category_info")))
A "belongs_to" relation is the inverse of a "has_many", it is used when the ID is on the same table as the field being loaded.
type Author struct {
AuthorID string `db:"author_id" tmeta:"pk"`
NomDePlume string `db:"nom_de_plume"`
}
type Book struct {
BookID string `db:"book_id" tmeta:"pk"`
AuthorID string `db:"author_id"`
Author *Author `db:"-" tmeta:"belongs_to"`
Title string `db:"title"`
}
bookT := b.For(Book{})
assert.NoError(b.MustSelectRelation(&book, "author").
LoadOne(bookT.RelationTargetPtr(&book, "author")))
A "belongs_to_many" relation is used for a many-to-many relation, using a join table.
type Book struct {
BookID string `db:"book_id" tmeta:"pk"`
Title string `db:"title"`
CategoryList []Category `db:"-" tmeta:"belongs_to_many,join_name=book_category"`
}
// BookCategory is the join table
type BookCategory struct {
BookID string `db:"book_id" tmeta:"pk"`
CategoryID string `db:"category_id" tmeta:"pk"`
}
type Category struct {
CategoryID string `db:"category_id" tmeta:"pk"`
Name string `db:"name"`
}
_, err = b.MustSelectRelation(&book, "category_list").
Load(bookT.RelationTargetPtr(&book, "category_list"))
A "belongs_to_many_ids" relation is similar to a "belongs_to_many" but instead of the field being a slice of structs, it is a slice of IDs, and methods are provided to easily synchronize the join table.
type Book struct {
BookID string `db:"book_id" tmeta:"pk"`
Title string `db:"title"`
CategoryIDList []string `db:"-" tmeta:"belongs_to_many_ids,join_name=book_category"`
}
// BookCategory is the join table
type BookCategory struct {
BookID string `db:"book_id" tmeta:"pk"`
CategoryID string `db:"category_id" tmeta:"pk"`
}
type Category struct {
CategoryID string `db:"category_id" tmeta:"pk"`
Name string `db:"name"`
}
// set the list of IDs in the join
book.CategoryIDList = []string{"category_0001","category_0002"}
// to synchronize we first delete any not in the new set
err = b.ExecOK(b.MustDeleteRelationNotIn(&book, "category_id_list"))
// and then insert (with ignore) the set
err = b.ExecOK(b.MustInsertRelationIgnore(&book, "category_id_list"))
// and you load it like any other relation
book.CategoryIDList = nil
_, err = b.MustSelectRelation(&book, "category_id_list").
Load(bookT.RelationTargetPtr(&book, "category_id_list"))
When selecting relations, the query builder needs the object to inspect and the name of the relation. However, the call to actually execute the select statement also needs to know the "target" field to load into.
The examples above use SelectRelation
and RelationTargetPtr
pointer to do this. This allows you to dynamically load a relation by only knowing it's name. If you are hard coding for a specific relation you can pass a pointer to the field itself. SelectRelationPtr
is also available and returns the pointer when the query is built. Pick your poison.
Create and update timestamps will be updated at the appropriate time with a simple call to the appropriate method on your struct, if it exists:
// called on insert
func (w *Widget) CreateTimeTouch() { ... }
// called on update
func (w *Widget) UpdateTimeTouch() { ... }
That part is easy.
To get date-time information to work correctly on multiple databases/drivers and make use of Go's time.Time
, marshal to/from JSON correctly, have subsecond precision, is human readable, and avoid time zone confusion, it is useful to make a custom type that deals with these concerns.
In SQLite3 use TEXT
as the column type, in Postgres use DATETIME
and in MySQL use DATETIME(6)
(requires 5.6.4 or above for subsecond precision).
Here is a recommendation on a type that accomplishes this:
func NewDBTime() DBTime {
return DBTime{Time: time.Now()}
}
type DBTime struct {
time.Time
}
func (t DBTime) Value() (driver.Value, error) {
if t.IsZero() {
return nil, nil
}
// use UTC to avoid time zone ambiguity
return t.Time.UTC().Format(`2006-01-02T15:04:05.999999999`), nil
}
func (t *DBTime) Scan(value interface{}) error {
if value == nil {
t.Time = time.Time{}
return nil
}
var s string
switch v := value.(type) {
case string:
s = v
case []byte:
s = string(v)
default:
return fmt.Errorf("DBTime.Scan: unable to scan type %T", value)
}
// MySQL uses a space instead of a "T", replace before parsing
s = strings.Replace(s, " ", "T", 1)
var err error
// use UTC to avoid time zone ambiguity
t.Time, err = time.ParseInLocation(`2006-01-02T15:04:05.999999999`, s, time.UTC)
// switch to local time zone
t.Time = t.Time.Local()
return err
}
You can then use this on your model object for any timestamps you need, including the create and update time:
type Widget struct {
// ...
CreateTime DBTime `db:"create_time"`
UpdateTime DBTime `db:"update_time"`
// ...
}
func (w *Widget) CreateTimeTouch() { w.CreateTime = NewDBTime() }
func (w *Widget) UpdateTimeTouch() { w.UpdateTime = NewDBTime() }
Optimistic locking means there is a version field on your table and when you perform an update it checks that the version did not change since you selected it earlier.
UpdateByID will generate SQL that checks for the previous version and increments to the next number. If zero rows are matched, you know the record has been modified since (or deleted). In this case, the correct thing to do is inform the original caller of the problem so the user can fix it (by refreshing the page, etc.)
ResultWithOneUpdate makes this simple. Example:
err = b.ResultWithOneUpdate(b.MustUpdateByID(&theRecord).Exec())
In this case theRecord
was read earlier, some fields were modified and it's being updated now. If not exactly one record was updated, ErrUpdateFailed
will be returned.
Some convenience methods are included on Builder which should reduce unneeded checks for common cases.
Must
will panic instead of returning an error, but note that this is only on the query building itself, not on query execution. Panics should only occur if you give it wrong type information or have incorrect struct tags. So the panic cases will be due to developer error, not runtime environment or user input, so using Must
and panicing in this case can be an acceptable tradeoff for the convenience it provides (being able to chain more stuff in a single expression).ResultWithOneUpdate
will check that your query returned exactly one row.ResultWithInsertID
will populate the ID that was inserted into the primary key of your struct.ResultOK
will ignore the result and only return the error (provided for convenient method chaining).ExecOK
will run Exec
, discard the sql.Result and just return the error.ExecContextOK
is like ExecOK
but accepts a context.Context also.Primary can be strings or auto-increment integers. We recommend using string UUIDs. The package gouuidv6 provides a way to make IDs that are globally unique, sort by creation time and are relatively short. UUIDs are more resilient architectural changes in long-lived projects (sharding, database synchronization problems, clustered servers, etc.)
Auto-increment works just fine as well.
Multiple primary keys are supported (and necessary for join tables). Not well tested on non-join tables, buyer beware.
Generally tmeta
doesn't care how it is called. If you want, you can use it directly in your HTTP handlers to build and run queries. However, we recommend you construct a "store" that acts as a data access layer on top of your database tables and house the query construction and execution in there. Your store would accept and return pointers to "model" objects (a struct that corresponds to your table(s)).
This is roughly analogous to the Data Access Object design pattern, although the point is to organize your code so queries can be maintained with in a specific section of the code, not to zealously adhere to this pattern.
Store methods would contain basic CRUD operations, plus any other more complex cases. Anything that requires it's own transaction goes as a method on the store. These days, store methods should also accept and use a context.Context
, this allows cancellation at a higher layer (for example the HTTP client closed the connection) to cause pending database queries to be cancelled. Fancy.
Your HTTP handlers/controllers, etc. can then use this store to access things.
Here's an example:
type Store struct {
*tmeta.Meta
*dbr.Connection
}
func (s *Store) CreateWidget(ctx context.Context, o *Widget) error {
tx, err := s.Connection.NewSession(nil).BeginTx(ctx)
if err != nil {
return err
}
defer tx.RollbackUnlessCommitted()
b := tmetadbr.New(tx, s.Meta)
o.WidgetID = gouuidv6.NewB64().String() // however you want to create your IDs
err = b.ResultWithOneUpdate(b.MustInsert(o).ExecContext(ctx))
if err != nil {
return err
}
return tx.Commit()
}
// ...
Often there is only one tmeta.Meta
in your application, but in large apps you can have sections of tables that only need to be aware of each other, in this case just make a new one (tmeta.New()
) for each, and each store would have one.
Some fancy things you can do with your relations include:
"Eager loading" is a feature of some ORMs, and we can achieve equivalent (less magical) functionality in our Store by simply adding SelectRelation
calls in the appropriate "read" method:
func (s *Store) FindWidgetByID(ctx context.Context, widgetID string) (*Widget, error) {
tx, err := s.Connection.NewSession(nil).BeginTx(ctx)
if err != nil {
return err
}
defer tx.RollbackUnlessCommitted()
b := tmetadbr.New(tx, s.Meta)
// load the widget
var widget Widget
err = b.MustSelectByID(&widget, widgetID).LoadOneContext(ctx, &widget)
if err != nil {
return err
}
// eager load the "category_list" relation
_, err = b.MustSelectRelation(&widget, "category_list").
Load(s.Meta.For(widget).RelationTargetPtr(&widget, "category_list"))
if err != nil {
return err
}
return tx.Commit()
}
It can be useful to allow other layers to request one or more relations by name. The store can then load these relations as requested and can also easily implement aliases for useful variations. The names should be filtered to avoid callers requesting too much data. Example:
func (s *Store) LoadWidgetRelations(ctx context.Context, widget *Widget, relationNames ...string) error {
tx, err := s.Connection.NewSession(nil).BeginTx(ctx)
if err != nil {
return err
}
defer tx.RollbackUnlessCommitted()
b := tmetadbr.New(tx, s.Meta)
for _, rn := range relationNames {
switch rn {
// these relations we just load as-is
case "category_list", "category_id_list":
_, err = b.MustSelectRelation(widget, rn).
Load(s.Meta.For(widget).RelationTargetPtr(widget, rn))
if err != nil {
return err
}
// it can be useful to provide more magical names with specific queries like this
case "change_list_last10":
_, err = b.MustSelectRelation(widget, "change_list").
Where("change_type = ?", "normal").
OrderDesc("create_time").
Limit(10).
Load(s.Meta.For(widget).RelationTargetPtr(widget, "change_list"))
if err != nil {
return err
}
default:
return fmt.Errorf("unkown (or disallowed) relation %q", rn)
}
}
return tx.Commit()
Another common relational pattern is to have a join table that needs to be updated to "match this set of IDs". If a Widget has a many-to-many join to Category (using a join table), you could easily synchronize the IDs in your update method like so:
func (s *Store) UpdateWidget(ctx context.Context, widget *Widget) error {
tx, err := s.Connection.NewSession(nil).BeginTx(ctx)
if err != nil {
return err
}
defer tx.RollbackUnlessCommitted()
b := tmetadbr.New(tx, s.Meta)
// update widget record
err = b.ResultWithOneUpdate(b.MustUpdateByID(widget).Exec())
if err != nil {
return err
}
// to synchronize we first delete any not in the new set
err = b.ExecOK(b.MustDeleteRelationNotIn(widget, "category_id_list"))
if err != nil {
return err
}
// and then insert (with ignore) the set
err = b.ExecOK(b.MustInsertRelationIgnore(widget, "category_id_list"))
if err != nil {
return err
}
return tx.Commit()
As a general rule, you can set whatever specific names you want in tmeta. The "Name" corresponding to a struct is by default it's snake-cased translation of the struct name. So "WidgetFactory" has a "Name" of "widget_factory". The "SQLName" is the name of the table in the database, and by default it is the same as Name, but is easily changable. Any time you reference a table in your code however you should do so using it's Name, and then you can SQLName() to get the actual table name.
The convention encouraged by tmeta is to avoid pluralization pretty much whenever possible. Translating "Category" into "Categories" and taking into account variations in how pluralization is done in English and in other languages can be non-trivial, is not positive (error prone) and has little benefit. It's way better to just say "Category" everywhere.
For fields that are a list, the suggested approach is to append "List". So you get "CategoryList" and ("category_list", the SQL field name equivalent).
This convention makes it a lot easier to match things up, because they have the same exact name everywhere.
Also, rather than having tables with just an "id" column tmeta encourages prefixing the identifier with the logical name of the object, e.g. a "Widget" struct has the name "widget" and it's primary key is "widget_id". This makes it so that anywhere a widget is referenced it is clearly an identfier for that type. Otherwise everytime you look at an "id" field you have to figure out what is being identified.
This package is currently on version 0, so there is no official guarantee of API compatibility. However, quite a bit of thought was put into how the API is organized and breaking changes will not be made lightly. v0.x.y tags will be made as changes are done and can be used with your package versioning tool of choice. After sufficient experience and feedback is gathered, a v1 release will be made.
FAQs
Unknown package
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Vite releases Rolldown-Vite, a Rust-based bundler preview offering faster builds and lower memory usage as a drop-in replacement for Vite.
Research
Security News
A malicious npm typosquat uses remote commands to silently delete entire project directories after a single mistyped install.
Research
Security News
Malicious PyPI package semantic-types steals Solana private keys via transitive dependency installs using monkey patching and blockchain exfiltration.