GO Entity or just "GOE" is a type safe ORM for Go
GOE logo by Luanexs
- go v1.24 or above;
Check out the Benchmarks section for a overview on GOE performance compared to other packages like, ent, GORM, sqlc, and others.
- 🚫 Non-String Usage;
- write queries with only Go code
- 🔖 Type Safety;
- get errors on compile time
- 📦 Auto Migrations;
- automatic generate tables from your structs
- 📄 SQL Like queries;
- use Go code to write queries with well known functions
- 🗂️ Iterator
- range over function to iterate over the rows
- 📚 Pagination
- paginate your large selects with just a function call
- ♻️ Wrappers
- wrappers for simple queries and builders for complex ones
go get github.com/go-goe/goe
As any database/sql support in go, you have to get a specific driver for your database, check Available Drivers
go get github.com/go-goe/postgres
type Animal struct {
// animal fields
}
type Database struct {
Animal *Animal
*goe.DB
}
dns := "user=postgres password=postgres host=localhost port=5432 database=postgres"
db, err := goe.Open[Database](postgres.Open(dns, postgres.Config{}))
go get github.com/go-goe/sqlite
type Animal struct {
// animal fields
}
type Database struct {
Animal *Animal
*goe.DB
}
db, err := goe.Open[Database](sqlite.Open("goe.db", sqlite.Config{}))
package main
import (
"fmt"
"github.com/go-goe/goe"
"github.com/go-goe/sqlite"
)
type Animal struct {
ID int
Name string
Emoji string
}
type Database struct {
Animal *Animal
*goe.DB
}
func main() {
db, err := goe.Open[Database](sqlite.Open("goe.db", sqlite.Config{}))
if err != nil {
panic(err)
}
defer goe.Close(db)
err = goe.Migrate(db).AutoMigrate()
if err != nil {
panic(err)
}
err = goe.Delete(db.Animal).All()
if err != nil {
panic(err)
}
animals := []Animal{
{Name: "Cat", Emoji: "🐈"},
{Name: "Dog", Emoji: "🐕"},
{Name: "Rat", Emoji: "🐀"},
{Name: "Pig", Emoji: "🐖"},
{Name: "Whale", Emoji: "🐋"},
{Name: "Fish", Emoji: "🐟"},
{Name: "Bird", Emoji: "🐦"},
}
err = goe.Insert(db.Animal).All(animals)
if err != nil {
panic(err)
}
animals, err = goe.List(db.Animal).AsSlice()
if err != nil {
panic(err)
}
fmt.Println(animals)
}
type Database struct {
User *User
Role *Role
UserLog *UserLog
*goe.DB
}
In goe, it's necessary to define a Database struct, this struct implements *goe.DB and a pointer to all the structs that's it's to be mappend.
It's through the Database struct that you will interact with your database.
GOE supports any type that implements the Scanner Interface. Most common are sql.Null types from database/sql package.
type Table struct {
Price decimal.Decimal `goe:"type:decimal(10,4)"`
NullId sql.Null[uuid.UUID] `goe:"type:uuid"`
NullString sql.NullString `goe:"type:varchar(100)"`
}
type User struct {
Id uint //this is primary key
Login string
Password string
}
Note
By default the field "Id" is primary key and all ids of integers are auto increment.
type User struct {
Identifier uint `goe:"pk"`
Login string
Password string
}
In case you want to specify a primary key use the tag value "pk".
type User struct {
Id string `goe:"pk;type:uuid"`
Login string `goe:"type:varchar(10)"`
Name string `goe:"type:varchar(150)"`
Password string `goe:"type:varchar(60)"`
}
You can specify a type using the tag value "type"
type User struct {
Id int
Name string
Email *string // this will be a null column
Phone sql.NullString `goe:"type:varchar(20)"` // also null
}
Important
A pointer is considered a null column in Database.
type User struct {
Id int
Name string
Email *string
CreatedAt time.Time `goe:"default:current_timestamp"`
}
To ensure that a default value will be used, call the IgnoreFields
on the Insert
function, otherwise GOE will try to insert the value stored on the field.
err = goe.Insert(db.User).IgnoreFields(&db.User.CreatedAt).One(&u)
In goe relational fields are created using the pattern TargetTable+TargetTableId, so if you want to have a foreign key to User, you will have to write a field like "UserId" or "IdUser".
type User struct {
Id uint
Login string
Name string
Password string
}
type UserDetails struct {
Id uint
Email string
Birthdate time.Time
UserId uint // one to one with User
}
For simplifications all relational slices should be the last fields on struct.
type User struct {
Id uint
Name string
Password string
UserLogs []UserLog // one User has many UserLogs
}
type UserLog struct {
Id uint
Action string
DateTime time.Time
UserId uint // if remove the slice from user, will became a one to one
}
The difference from one to one and many to one it's the add of a slice field on the "many" struct
For simplifications all relational slices should be the last fields on struct.
type User struct {
Id uint
Name string
Password string
UserRoles []UserRole
}
type UserRole struct {
UserId uint `goe:"pk"`
RoleId uint `goe:"pk"`
}
type Role struct {
Id uint
Name string
UserRoles []UserRole
}
Is used a combination of two many to one to generate a many to many. In this example, User has many UserRole and Role has many UserRole.
It's used the tags "pk" for ensure that the foreign keys will be both primary key.
One to Many
type Page struct {
Id int
Number int
PageId *int
Pages []Page
}
One to One
type Page struct {
Id int
Number int
PageId *int
}
type User struct {
Id uint
Name string
Email string `goe:"unique"`
}
To create a unique index you need the "unique" goe tag
type User struct {
Id uint
Name string
Email string `goe:"index"`
}
To create a common index you need the "index" goe tag
type User struct {
Id uint
Name string `goe:"index(n:idx_name_status)"`
Email string `goe:"index(n:idx_name_status);unique"`
}
Using the goe tag "index()", you can pass the index infos as a function call. "n:" is a parameter for name, to have a two column index just need two indexes with same name. You can use the semicolon ";" to create another single index for the field.
type User struct {
Id uint
Name string `goe:"index(unique n:idx_name_status)"`
Email string `goe:"index(unique n:idx_name_status);unique"`
}
Just as creating a Two Column Index but added the "unique" value inside the index function.
On GOE it's possible to create schemas by the database struct, all schemas should have the suffix Schema
or a tag goe:"schema"
.
type User struct {
...
}
type UserRole struct {
...
}
type Role struct {
...
}
// schema with suffix Schema
type UserSchema struct {
User *User
UserRole *UserRole
Role *Role
}
// schema with any name
type Authentication struct {
User *User
UserRole *UserRole
Role *Role
}
type Database struct {
Status *Status // status will be on the default schema
*UserSchema // all structs on UserSchema will be created inside user schema
*Authentication `goe:"schema"` // will create Authentication schema
*goe.DB
}
Tip
On SQLite any schema will be a new attached db file.
GOE supports any logger that implements the Logger interface
type Logger interface {
InfoContext(ctx context.Context, msg string, kv ...any)
WarnContext(ctx context.Context, msg string, kv ...any)
ErrorContext(ctx context.Context, msg string, kv ...any)
}
The logger is defined on database opening
db, err := goe.Open[Database](sqlite.Open("goe.db", sqlite.Config{
DatabaseConfig: goe.DatabaseConfig{
Logger: slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})),
IncludeArguments: true,
QueryThreshold: time.Second},
}))
Tip
You can use slog as your standard logger or make a adapt over the Logger interface.
To open a database use goe.Open
function, it's require a valid driver. Most of the drives will require a dns/path connection and a config setup. On goe.Open
needs to specify the struct database.
If you don't need the database connection anymore, call goe.Close
to ensure that all the database resources will be removed from memory.
type Database struct {
Animal *Animal
AnimalFood *AnimalFood
Food *Food
*goe.DB
}
dns := "user=postgres password=postgres host=localhost port=5432 database=postgres"
db, err := goe.Open[Database](postgres.Open(dns, postgres.Config{}))
if err != nil {
// handler error
}
To auto migrate the structs, use the goe.Migrate(db).AutoMigrate()
passing the database returned by goe.Open
.
// migrate all database structs
err = goe.Migrate(db).AutoMigrate()
if err != nil {
// handler error
}
type Select struct {
ID int
Name string
}
type Database struct {
Select *Select
*goe.DB
}
err = goe.Migrate(db).OnTable("Select").RenameColumn("Name", "NewName")
if err != nil {
// handler error
}
err = goe.Migrate(db).OnTable("Select").DropColumn("NewName")
if err != nil {
// handler error
}
err = goe.Migrate(db).OnTable("Select").RenameTable("NewSelect")
if err != nil {
// handler error
}
err = goe.Migrate(db).OnTable("NewSelect").DropTable()
if err != nil {
// handler error
}
GOE drivers supports a output migrate path to specify a directory to store the generated SQL. In this way, calling the "AutoMigrate" function goe WILL NOT auto apply the migrations and output the result as a sql file in the specified path.
// open the database with the migrate path config setup
db, err := goe.Open[Database](sqlite.Open("goe.db", sqlite.Config{
MigratePath: "migrate/",
}))
if err != nil {
// handler error
}
// AutoMigrate will output the result as a sql file, and not auto apply the migration
err = goe.Migrate(db).AutoMigrate()
if err != nil {
// handler error
}
In this example the file will be output in the "migrate/" path, as follow:
📂 migrate
| ├── SQLite_1760042267.sql
go.mod
Tip
Any other migration like "DropTable", "RenameColumn" and others... will have the same result as "AutoMigrate", and will generate the SQL file.
Find is used when you want to return a single result.
// one primary key
animal, err = goe.Find(db.Animal).ById(Animal{Id: 2})
// two primary keys
animalFood, err = goe.Find(db.AnimalFood).ById(AnimalFood{IdAnimal: 3, IdFood: 2})
// find record by value, if have more than one it will returns the first
cat, err = goe.Find(db.Animal).ByValue(Animal{Name: "Cat"})
Tip
Use goe.FindContext for specify a context.
Tip
Use OnErrNotFound to replace ErrNotFound with a new error.
List has support for OrderBy, Pagination and Joins.
// list all animals
animals, err = goe.List(db.Animal).AsSlice()
// list the animals with name "Cat", Id "3" and IdHabitat "4"
animals, err = goe.List(db.Animal).Filter(Animal{Name: "Cat", Id: 3, IdHabitat: 4}).AsSlice()
// when using % on filter, goe makes a like operation
animals, err = goe.List(db.Animal).Filter(Animal{Name: "%Cat%"}).AsSlice()
Tip
Use goe.ListContext for specify a context.
Return all animals as a slice
// select * from animals
animals, err = goe.List(db.Animal).AsSlice()
if err != nil {
// handler error
}
Tip
Use goe.SelectContext for specify a context.
Iterate over the rows
for row, err := range goe.List(db.Animal).Rows() {
// iterator rows
}
var result []struct {
User string
Role *string
EndTime *time.Time
}
// row is the generic struct
for row, err := range goe.Select[struct {
User string // output row
Role *string // output row
EndTime *time.Time // output row
}](&struct {
User *string // table column
Role *string // table column
EndTime **time.Time // table column
}{
User: &db.User.Name,
Role: &db.Role.Name,
EndTime: &db.UserRole.EndDate,
}).
Joins(
join.LeftJoin[int](&db.User.Id, &db.UserRole.UserId),
join.LeftJoin[int](&db.UserRole.RoleId, &db.Role.Id),
).
OrderByAsc(&db.User.Id).Rows() {
if err != nil {
//handler error
}
//handler rows
result = append(result, row)
}
For specific field is used a new struct, each new field guards the reference for the database attribute.
For where, goe uses a sub-package where, on where package you have all the goe available where operations.
animals, err = goe.List(db.Animal).Where(where.Equals(&db.Animal.Id, 2)).AsSlice()
if err != nil {
//handler error
}
It's possible to group a list of where operations inside Where()
animals, err = goe.List(db.Animal).Where(
where.And(
where.LessEquals(&db.Animal.Id, 2),
where.In(&db.Animal.Name, []string{"Cat", "Dog"}),
),
).AsSlice()
if err != nil {
//handler error
}
You can use a if to call a where operation only if it's match
selectQuery := goe.List(db.Animal).Where(where.LessEquals(&db.Animal.Id, 30))
if filter.In {
selectQuery = selectQuery.Where(
where.And(
where.LessEquals(&db.Animal.Id, 30),
where.In(&db.Animal.Name, []string{"Cat", "Dog"}),
),
)
}
animals, err = selectQuery.AsSlice()
if err != nil {
//handler error
}
It's possible to use a query inside a where.In
// use AsQuery() for get a result as a query
querySelect := goe.Select[any](&struct{ Name *string }{Name: &db.Animal.Name}).
Joins(
join.Join[int](&db.Animal.Id, &db.AnimalFood.IdAnimal),
join.Join[uuid.UUID](&db.AnimalFood.IdFood, &db.Food.Id)).
Where(
where.In(&db.Food.Name, []string{foods[0].Name, foods[1].Name})).
AsQuery()
// where in with another query
a, err := goe.List(db.Animal).Where(where.In(&db.Animal.Name, querySelect)).AsSlice()
if err != nil {
//handler error
}
On where, GOE supports operations on two columns, all where operations that have Arg
as suffix it's used for operation on columns.
In the example, the operator greater (>) on the columns Score and Minimum is used to return all exams that have a score greater than the minimum.
err = goe.List(db.Exam).
Where(where.GreaterArg[float32](&db.Exam.Score, &db.Exam.Minimum)).AsSlice()
On join, goe uses a sub-package join, on join package you have all the goe available join operations.
For the join operations, you need to specify the type, this make the joins operations more safe. So if you change a type from a field, the compiler will throw a error.
animals, err = goe.List(db.Animal).
Joins(
join.Join[int](&db.Animal.Id, &db.AnimalFood.IdAnimal),
join.Join[uuid.UUID](&db.Food.Id, &db.AnimalFood.IdFood),
).AsSlice()
if err != nil {
//handler error
}
Same as where, you can use a if to only make a join if the condition match.
For OrderBy you need to pass a reference to a mapped database field.
It's possible to OrderBy desc and asc. List and Select has support for OrderBy queries.
animals, err = goe.List(db.Animal).OrderByDesc(&db.Animal.Id).AsSlice()
if err != nil {
//handler error
}
animals, err = goe.List(db.Animal).OrderByAsc(&db.Animal.Id).AsSlice()
if err != nil {
//handler error
}
For pagination, it's possible to run on Select and List functions
// page 1 of size 10
page, err = goe.List(db.Animal).AsPagination(1, 10)
if err != nil {
//handler error
}
// page 1 of size 10
page, err = goe.List(db.Animal).AsPagination(1, 10)
if err != nil {
//handler error
}
Note
AsPagination default values for page and size are 1 and 10 respectively.
For aggregates goe uses a sub-package aggregate, on aggregate package you have all the goe available aggregates.
On select fields, goe uses query sub-package for declaring a aggregate field on struct.
result, err := goe.Select[struct {
query.Count
}](&struct{
*query.Count
}{
aggregate.Count(&db.Animal.Id)
}).AsSlice()
if err != nil {
// handler error
}
// count value as int64
result[0].Value
For functions goe uses a sub-package function, on function package you have all the goe available functions.
On select fields, goe uses query sub-package for declaring a function result field on struct.
for row, err := range goe.Select[struct {
UpperName query.Function[string]
}](&struct {
UpperName *query.Function[string]
}{
UpperName: function.ToUpper(&db.Animal.Name),
}).Rows() {
if err != nil {
//handler error
}
//function result value
row.UpperName.Value
}
Functions can be used inside where.
animals, err = goe.List(db.Animal).
Where(
where.Like(function.ToUpper(&db.Animal.Name), "%CAT%")
).AsSlice()
if err != nil {
//handler error
}
Note
where like expected a second argument always as string.
animals, err = goe.List(db.Animal).
Where(
where.Equals(function.ToUpper(&db.Animal.Name), function.Argument("CAT")),
).AsSlice()
if err != nil {
//handler error
}
Important
to by pass the compiler type warning, use function.Argument. This way the compiler will check the argument value.
On Insert if the primary key value is auto-increment, the new Id will be stored on the object after the insert.
a := Animal{Name: "Cat", Emoji: "🐘"}
err = goe.Insert(db.Animal).One(&a)
if err != nil {
//handler error
}
// new generated id
a.Id
Tip
Use goe.InsertContext for specify a context.
foods := []Food{
{Name: "Meat", Emoji: "🥩"},
{Name: "Hotdog", Emoji: "🌭"},
{Name: "Cookie", Emoji: "🍪"},
}
err = goe.Insert(db.Food).All(foods)
if err != nil {
//handler error
}
Tip
Use goe.InsertContext for specify a context.
Save is the basic function for updates a single record; only updates the non-zero values.
a := Animal{Id: 2}
a.Name = "Update Cat"
// update animal of id 2
err = goe.Save(db.Animal).ByValue(a)
if err != nil {
//handler error
}
Tip
Use goe.SaveContext for specify a context.
Update with set uses update sub-package. This is used for more complex updates, like updating a field with zero/nil values or make a batch update.
a := Animal{Id: 2}
// a.IdHabitat is nil, so is ignored by Save
err = goe.Update(db.Animal).
Sets(update.Set(&db.Animal.IdHabitat, a.IdHabitat)).
Where(where.Equals(&db.Animal.Id, a.Id))
if err != nil {
//handler error
}
Check out the Where section for more information about where operations.
Caution
The where call ensures that only the matched rows will be updated.
Tip
Use goe.UpdateContext for specify a context.
Remove is used for remove only one record by primary key
// remove animal of id 2
err = goe.Remove(db.Animal).ById(Animal{Id: 2})
if err != nil {
//handler error
}
Tip
Use goe.RemoveContext for specify a context.
Delete all records from Animal
err = goe.Delete(db.Animal).All()
if err != nil {
//handler error
}
Delete all matched records
err = goe.Delete(db.Animal).Where(where.Like(&db.Animal.Name, "%Cat%"))
if err != nil {
//handler error
}
Check out the Where section for more information about where operations.
Caution
The where call ensures that only the matched rows will be deleted.
Tip
Use goe.DeleteContext for specify a context.
Setup the transaction with the database function db.NewTransaction()
tx, err = db.NewTransaction()
if err != nil {
// handler error
}
defer tx.Rollback()
You can use the OnTransaction()
function to setup a transaction for Select, Insert, Update and Delete.
Tip
Ensure to call defer tx.Rollback()
; this will make the Rollback happens if something go wrong
Tip
Use goe.NewTransactionContext for specify a context
To Commit a Transaction just call tx.Commit()
err = tx.Commit()
if err != nil {
// handler the error
}
To Rollback a Transaction just call tx.Rollback()
err = tx.Rollback()
if err != nil {
// handler the error
}
The isolation is used for control the flow and security of multiple transactions. On goe you can use the sql.IsolationLevel.
By default if you call db.NewTransaction()
it's use the Serializable isolation.
Source code of benchmarks can be find on lauro-santana/go-orm-benchmarks. The benchmarks will be update if any new feature is released for any of these packages. You are welcome to notice me if are anything new or wrong with the benchmarks on the repository.
Package | Avg n/op | Avg ns/op | Avg B/op | Avg allocs/op |
---|---|---|---|---|
pgx | 20655 | 57493 | 995 | 18 |
sqlc | 21656 | 57810 | 995 | 18 |
database/sql | 19466 | 61851 | 1720 | 47 |
goe | 17270 | 70941 | 4552 | 70 |
ent | 15342 | 76627 | 5152 | 135 |
bun | 12975 | 92397 | 5657 | 23 |
gorm | 10000 | 110914 | 5674 | 110 |
Package | Avg n/op | Avg ns/op | Avg B/op | Avg allocs/op |
---|---|---|---|---|
pgx | 1900.0 | 638138.0 | 52602.5 | 730.0 |
database/sql | 1754.0 | 681157.5 | 57922.8 | 1360.0 |
sqlc | 1666.0 | 706612.8 | 79800.8 | 770.0 |
goe | 1586.0 | 777239.3 | 73694.5 | 1130.0 |
ent | 1370.0 | 884408.0 | 111683.0 | 3010.0 |
bun | 1053.8 | 1177303.5 | 90663.5 | 1170.0 |
gorm | 957.0 | 1219613.0 | 100829.3 | 2580.0 |
Package | n/op | ns/op | B/op | allocs/op |
---|---|---|---|---|
sqlc | 2806.6 | 435619.4 | 288.0 | 8.0 |
pgx | 2649.4 | 437431.0 | 304.0 | 9.0 |
database/sql | 2546.6 | 437955.8 | 576.0 | 11.0 |
goe | 2673.0 | 450239.6 | 2734.2 | 40.0 |
ent | 2561.2 | 470270.8 | 3943.6 | 90.0 |
bun | 2400.0 | 484351.0 | 5090.4 | 15.0 |
gorm | 2464.4 | 487071.6 | 6110.2 | 91.0 |
Package | Avg n/op | Avg ns/op | Avg B/op | Avg allocs/op |
---|---|---|---|---|
pgx | 127.5 | 9032688.5 | 335594.5 | 2047.0 |
sqlc | 116.5 | 9784080.0 | 675911.0 | 12048.5 |
goe | 73.0 | 15777915.5 | 6059695.5 | 39879.0 |
database/sql | 70.0 | 16383613.5 | 6054050.5 | 39833.5 |
ent | 56.0 | 18343069.5 | 9805318.5 | 80389.0 |
gorm | 42.0 | 28939012.0 | 5829835.5 | 95750.0 |
bun | 39.5 | 28704378.5 | 1755007.5 | 8036.5 |
database/sql has to append the strings and values by a external function, so you could make a optimized database/sql usage for insert bulk to get a result equivalent to pgx or sqlc.
Package | Avg n/op | Avg ns/op | Avg B/op | Avg allocs/op |
---|---|---|---|---|
pgx | 2666.5 | 453922.5 | 329.25 | 9.0 |
database/sql | 2533.0 | 455223.0 | 632.0 | 11.0 |
sqlc | 2707.75 | 466297.0 | 313.5 | 8.0 |
goe | 2421.25 | 488427.0 | 3108.25 | 37.0 |
bun | 2263.25 | 506721.75 | 4789.0 | 6.0 |
gorm | 2491.5 | 541869.25 | 7489.0 | 99.0 |
ent | 1782.75 | 649685.0 | 7462.0 | 183.0 |
Package | n/op | ns/op | B/op | allocs/op |
---|---|---|---|---|
sqlc | 2537.8 | 447178.6 | 112.0 | 3.2 |
pgx | 2640.0 | 456962.2 | 127.0 | 4.0 |
database/sql | 2548.2 | 464805.0 | 206.8 | 6.0 |
goe | 2522.6 | 469782.4 | 1531.0 | 20.0 |
ent | 2621.6 | 470532.0 | 1808.4 | 40.0 |
bun | 2488.0 | 486804.6 | 4668.6 | 5.0 |
gorm | 2359.4 | 494844.2 | 3075.8 | 44.0 |