Build Your Own Go ORM: Logger, Struct Mapping, CRUD, Transactions & Hooks

This tutorial walks through creating a lightweight Go ORM—covering a custom logger, converting structs to table metadata, implementing basic CRUD operations, handling transactions, and adding hook callbacks—while exposing the core Session data structure and providing a GitHub reference.

Nullbody Notes
Nullbody Notes
Nullbody Notes
Build Your Own Go ORM: Logger, Struct Mapping, CRUD, Transactions & Hooks

Project is a stripped‑down version of GORM that demonstrates the internal behavior of each ORM function.

Repository: https://github.com/gofish2020/easyorm

Core Data Structure

The central type is Session:

type Session struct {
    db *sql.DB
    tx *sql.Tx

    // table
    tableMeta *table.TableMeta

    dial dialect.Dialect
    // clause
    clause clause.Clause

    // sql query
    sql strings.Builder
    // sql param
    sqlParam []interface{}
}

db : executes SQL statements.

tx : executes SQL within a transaction.

tableMeta : metadata derived from a Go struct.

clause : builds CRUD SQL strings.

dial : converts Go types to database types.

sql / sqlParam : the SQL statement and its parameters ready for execution.

Custom Logger

The logger prints messages in different colors according to log level.

logger example
logger example

Struct‑to‑Table Metadata

// Parse converts a struct into table/field information
func Parse(dest interface{}, d dialect.Dialect) *TableMeta {
    if dest == nil {
        return nil
    }
    value := reflect.ValueOf(dest)
    if value.Kind() == reflect.Ptr && value.IsNil() {
        value = reflect.New(value.Type().Elem()) // create a new instance
    }
    meta := &TableMeta{
        Model:    value.Interface(),
        fieldMap: make(map[string]*Field),
    }
    destValue := reflect.Indirect(value)
    destType := destValue.Type()
    if m, ok := value.Interface().(Tabler); ok {
        meta.TableName = m.TableName()
    } else {
        meta.TableName = destType.Name()
    }
    for i := 0; i < destType.NumField(); i++ {
        fieldType := destType.Field(i)
        if !fieldType.Anonymous && fieldType.IsExported() {
            field := &Field{
                FieldName: fieldType.Name,
                FieldType: d.ConvertType2DBType(destValue.Field(i)),
                FieldTag:  fieldType.Tag.Get("easyorm"),
            }
            meta.Fields = append(meta.Fields, field)
            meta.FieldsName = append(meta.FieldsName, field.FieldName)
            meta.fieldMap[field.FieldName] = field
        }
    }
    return meta
}

CRUD – Insert Example

Typical call:

s.Insert(&User{}, &User{})
func (s *Session) Insert(values ...interface{}) (int64, error) {
    if len(values) == 0 {
        return 0, errors.New("param is empty")
    }
    // init table
    s.Model(values[0])
    // clause insert: generate "INSERT INTO $table ($fields)"
    s.clause.Set(clause.INSERT, s.TableMeta().TableName, s.TableMeta().FieldsName)

    valueSlice := []interface{}{}
    for _, value := range values {
        s.CallMethod(BeforeInsert, value)
        // extract field values as SQL parameters
        valueSlice = append(valueSlice, s.TableMeta().ExtractFieldValue(value))
    }
    // clause values: generate "VALUES (?,?,?),(?,?,?)"
    s.clause.Set(clause.VALUES, valueSlice...)
    // build final SQL
    sql, sqlParam := s.clause.Build(clause.INSERT, clause.VALUES)
    // execute
    result, err := s.Raw(sql, sqlParam...).Exec()
    if err != nil {
        return 0, err
    }
    s.CallMethod(AfterInsert, nil)
    return result.RowsAffected()
}

Transaction Support

// TxFunc will be called between tx.Begin() and tx.Commit()
type TxFunc func(*session.Session) (interface{}, error)

// Transaction executes SQL wrapped in a transaction and automatically commits if no error occurs
func (engine *Engine) Transaction(f TxFunc) (result interface{}, err error) {
    s := engine.NewSession()
    if err = s.Begin(); err != nil {
        return nil, err
    }
    defer func() {
        if p := recover(); p != nil {
            _ = s.Rollback()
            panic(p)
        } else if err != nil {
            _ = s.Rollback()
        } else {
            err = s.Commit()
        }
    }()
    return f(s)
}

Hook Mechanism

Hooks are invoked via reflect.MethodByName and Call, allowing user‑defined methods to run before or after specific operations.

func (s *Session) CallMethod(method string, value interface{}) {
    fm := reflect.ValueOf(s.TableMeta().Model).MethodByName(method)
    if value != nil {
        fm = reflect.ValueOf(value).MethodByName(method)
    }
    param := []reflect.Value{reflect.ValueOf(s)}
    if fm.IsValid() {
        if v := fm.Call(param); len(v) > 0 {
            if err, ok := v[0].Interface().(error); ok {
                logger.Error(err)
            }
        }
    }
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

transactiondatabaseGoormhookgormlogger
Nullbody Notes
Written by

Nullbody Notes

Go backend development, learning open-source project source code together, focusing on simplicity and practicality.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.