Skip to content

Zima ORM library that is simple, ultra-fast and self-mockable for Go

License

Notifications You must be signed in to change notification settings

IceWhaleTech/zorm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zorm

license Go Report Card codecov FOSSA Status

🏎️ Zima ORM library that is simple, ultra-fast and self-mockable for Go

English | 中文

🚀 Latest Features

⚡ Reuse Function Enabled by Default - Revolutionary Performance Improvement

  • 8.6x Performance Improvement: Optimized caching mechanism, reduced redundant calculations
  • 92% Memory Optimization: Zero allocation design, significantly reduced GC pressure
  • Zero Configuration: Enabled by default, no additional setup required
  • Concurrent Safe: Supports high concurrency scenarios with stable performance

🗺️ Map Type Support

  • No Struct Definition Required: Directly use map[string]interface{} to operate database
  • Complete CRUD Support: Insert, Update, Select fully supported
  • Type Safety: Automatic type conversion and validation
  • SQL Optimization: Automatically generates efficient SQL statements

🏗️ Embedded Struct Support

  • Auto Expansion: Nested struct fields automatically expanded to SQL
  • Tag Support: Supports zorm tags for custom field names
  • Recursive Processing: Supports multi-level nested structs
  • Performance Optimization: Field mapping cache, avoiding redundant calculations

⏰ Faster and More Accurate Time Parsing

  • 5.1x Performance Improvement: Smart format detection, single parse
  • 100% Memory Optimization: Zero allocation design, reduced memory usage
  • Multi-format Support: Standard format, timezone format, nanosecond format, date-only format
  • Empty Value Handling: Automatically handle empty strings and NULL values

Goals

  • Easy to use: SQL-Like (One-Line-CRUD)
  • KISS: Keep it small and beautiful (not big and comprehensive)
  • Universal: Support struct, map, pb and basic types
  • Testable: Support self-mock (because parameters as return values, most mock frameworks don't support)
    • A library that is not test-oriented is not a good library
  • As-Is: Try not to make hidden settings to prevent misuse
  • Solve core pain points:
    • Manual SQL is error-prone, data assembly takes too much time
    • time.Time cannot be read/written directly
    • SQL function results cannot be scanned directly
    • Database operations cannot be easily mocked
    • QueryRow's sql.ErrNoRows problem
    • Directly replace the built-in Scanner, completely take over data reading type conversion
  • Core principles:
    • Don't map a table to a model like other ORMs
    • (In zorm, you can use Fields filter to achieve this)
    • Try to keep it simple, map one operation to one model!
  • Other advantages:
    • More natural where conditions (only add parentheses when needed, compared to gorm)
    • In operation accepts various types of slices
    • Migration from other ORM libraries requires no historical code modification, non-invasive modification

Feature Matrix

Below is a comparison with mainstream ORM libraries (please don't hesitate to open issues for corrections)

Library zorm (me) gorm xorm Notes
Usability No type specification needed zorm doesn't need low-frequency DDL in tags
No model specification needed gorm/xorm modification operations need to provide "template"
No primary key specification needed gorm/xorm prone to misoperation, such as deleting/updating entire table
Low learning cost If you know SQL, you can use zorm
Reuse native connections zorm has minimal refactoring cost
Full type conversion maybe Eliminate type conversion errors
Reuse query commands zorm uses the same function for batch and single operations
Map type support Operate database with map Without defining struct
Testability Self-mock zorm is very convenient for unit testing
Performance Compared to native time <=1x 2~3x 2~3x xorm using prepare mode will be 2~3x slower
Reflection reflect2 reflect reflect zorm zero use of ValueOf
Cache Optimization 🚀 8.6x performance improvement, zero allocation design, smart call-site caching

Quick Start

  1. Import package

    import b "github.com/IceWhaleTech/zorm"
  2. Define Table object

    t := z.Table(d.DB, "t_usr")
    
    t1 := z.Table(d.DB, "t_usr", ctx)
  • d.DB is a database connection object that supports Exec/Query/QueryRow
  • t_usr can be a table name or nested query statement
  • ctx is the Context object to pass, defaults to context.Background() if not provided
  • Reuse functionality is enabled by default, providing 2-14x performance improvement, no additional configuration needed
  1. (Optional) Define model object

    // Info fields without zorm tag will not be fetched by default
    type Info struct {
       ID   int64  `zorm:"id"`
       Name string `zorm:"name"`
       Tag  string `zorm:"tag"`
    }
    
    // Call t.UseNameWhenTagEmpty() to use field names without zorm tag as database fields to fetch
  2. Execute operations

  • CRUD interfaces return (affected rows, error)

  • Type V is an abbreviation for map[string]interface{}, similar to gin.H

  • Insert

    // o can be object/slice/ptr slice
    n, err = t.Insert(&o)
    n, err = t.InsertIgnore(&o)
    n, err = t.ReplaceInto(&o)
    
    // Insert only partial fields (others use defaults)
    n, err = t.Insert(&o, z.Fields("name", "tag"))
    
    // Resolve primary key conflicts
    n, err = t.Insert(&o, z.Fields("name", "tag"),
       z.OnConflictDoUpdateSet([]string{"id"}, z.V{
          "name": "new_name",
          "age":  z.U("age+1"), // Use z.U to handle non-variable updates
       }))
    
    // Use map insert (no need to define struct)
    userMap := map[string]interface{}{
       "name":  "John Doe",
       "email": "[email protected]",
       "age":   30,
    }
    n, err = t.Insert(userMap)
    
    // Support embedded struct
    type User struct {
       Name  string `zorm:"name"`
       Email string `zorm:"email"`
       Address struct {
          Street string `zorm:"street"`
          City   string `zorm:"city"`
       } `zorm:"-"` // embedded struct
    }
    n, err = t.Insert(&user)
    
    // Support field ignore
    type User struct {
       Name     string `zorm:"name"`
       Password string `zorm:"-"` // ignore this field
       Email    string `zorm:"email"`
    }
    n, err = t.Insert(&user)
  • Select

    // o can be object/slice/ptr slice
    n, err := t.Select(&o,
       z.Where("name = ?", name),
       z.GroupBy("id"),
       z.Having(z.Gt("id", 0)),
       z.OrderBy("id", "name"),
       z.Limit(1))
    
    // Use basic type + Fields to get count (n value is 1, because result has only 1 row)
    var cnt int64
    n, err = t.Select(&cnt, z.Fields("count(1)"), z.Where("name = ?", name))
    
    // Also support arrays
    var ids []int64
    n, err = t.Select(&ids, z.Fields("id"), z.Where("name = ?", name))
    
    // Can force index
    n, err = t.Select(&ids, z.Fields("id"), z.IndexedBy("idx_xxx"), z.Where("name = ?", name))
  • Select to Map (no struct needed)

    // single row to map
    var m map[string]interface{}
    n, err := t.Select(&m, z.Fields("id", "name", "age"), z.Where(z.Eq("id", 1)))
    
    // multiple rows to []map
    var ms []map[string]interface{}
    n, err = t.Select(&ms, z.Fields("id", "name", "age"), z.Where(z.Gt("age", 18)))
  • Update

    // o can be object/slice/ptr slice
    n, err = t.Update(&o, z.Where(z.Eq("id", id)))
    
    // Use map update
    n, err = t.Update(z.V{
          "name": "new_name",
          "tag":  "tag1,tag2,tag3",
          "age":  z.U("age+1"), // Use z.U to handle non-variable updates
       }, z.Where(z.Eq("id", id)))
    
    // Use map update partial fields
    n, err = t.Update(z.V{
          "name": "new_name",
          "tag":  "tag1,tag2,tag3",
       }, z.Fields("name"), z.Where(z.Eq("id", id)))
    
    n, err = t.Update(&o, z.Fields("name"), z.Where(z.Eq("id", id)))
  • CRUD with Reuse (enabled by default)

    // Reuse is on by default; repeated calls at the same call-site reuse SQL/metadata
    // Update example
    type User struct { ID int64 `zorm:"id"`; Name string `zorm:"name"`; Age int `zorm:"age"` }
    for _, u := range users {
        _, _ = t.Update(&u, z.Fields("name", "age"), z.Where(z.Eq("id", u.ID)))
    }
    
    // Insert example
    for _, u := range users {
        _, _ = t.Insert(&u)
    }
  • Delete

    // Delete by condition
    n, err = t.Delete(z.Where("name = ?", name))
    n, err = t.Delete(z.Where(z.Eq("id", id)))
  • Variable conditions

    conds := []interface{}{z.Cond("1=1")} // prevent empty where condition
    if name != "" {
       conds = append(conds, z.Eq("name", name))
    }
    if id > 0 {
       conds = append(conds, z.Eq("id", id))
    }
    // Execute query operation
    n, err := t.Select(&o, z.Where(conds...))
  • Join queries

    type Info struct {
       ID   int64  `zorm:"t_usr.id"` // field definition with table name
       Name string `zorm:"t_usr.name"`
       Tag  string `zorm:"t_tag.tag"`
    }
    
    // Method 1
    t := z.Table(d.DB, "t_usr join t_tag on t_usr.id=t_tag.id") // table name with join statement
    var o Info
    n, err := t.Select(&o, z.Where(z.Eq("t_usr.id", id))) // condition with table name
    
    // Method 2
    t = z.Table(d.DB, "t_usr") // normal table name
    n, err = t.Select(&o, z.Join("join t_tag on t_usr.id=t_tag.id"), z.Where(z.Eq("t_usr.id", id))) // condition needs table name
  • Get inserted auto-increment id

    // First need database to have an auto-increment ID field
    type Info struct {
       ZormLastId int64 // add a field named ZormLastId of integer type
       Name       string `zorm:"name"`
       Age        string `zorm:"age"`
    }
    
    o := Info{
       Name: "OrcaZ",
       Age:  30,
    }
    n, err = t.Insert(&o)
    
    id := o.ZormLastId // get the inserted id
  • New features example: Map types and Embedded Struct

    // 1. Use map type (no need to define struct)
    userMap := map[string]interface{}{
       "name":     "John Doe",
       "email":    "[email protected]",
       "age":      30,
       "created_at": time.Now(),
    }
    n, err := t.Insert(userMap)
    
    // 2. Support embedded struct
    type Address struct {
       Street string `zorm:"street"`
       City   string `zorm:"city"`
       Zip    string `zorm:"zip"`
    }
    
    type User struct {
       ID      int64  `zorm:"id"`
       Name    string `zorm:"name"`
       Email   string `zorm:"email"`
       Address Address `zorm:"-"` // embedded struct
       Password string `zorm:"-"` // ignore field
    }
    
    user := User{
       Name:  "Jane Doe",
       Email: "[email protected]",
       Address: Address{
          Street: "123 Main St",
          City:   "New York",
          Zip:    "10001",
       },
       Password: "secret", // this field will be ignored
    }
    n, err := t.Insert(&user)
    
    // 3. Complex nested structure
    type Profile struct {
       Bio     string `zorm:"bio"`
       Website string `zorm:"website"`
    }
    
    type UserWithProfile struct {
       ID      int64  `zorm:"id"`
       Name    string `zorm:"name"`
       Profile Profile `zorm:"-"` // nested embedding
    }
  • Currently using other ORM frameworks (new interfaces can be switched first)

    // [gorm] db is a *gorm.DB
    t := z.Table(db.DB(), "tbl")
    
    // [xorm] db is a *xorm.EngineGroup
    t := z.Table(db.Master().DB().DB, "tbl")
    // or
    t := z.Table(db.Slave().DB().DB, "tbl")

Other Details

Table Options

Option Description
Debug Print SQL statements
Reuse Reuse SQL and storage based on call location (enabled by default, 2-14x improvement). Shape-aware multi-shape cache is built-in
NoReuse Disable Reuse functionality (not recommended, will reduce performance)
UseNameWhenTagEmpty Use field names without zorm tag as database fields to fetch
ToTimestamp Use timestamp for Insert, not formatted string

Option usage example:

n, err = t.Debug().Insert(&o)

n, err = t.ToTimestamp().Insert(&o)

// Reuse functionality is enabled by default, no manual call needed
// If you need to disable it (not recommended), you can call:
n, err = t.NoReuse().Insert(&o)

// Reuse is shape-aware by default: guards against SQL shape changes at the same call-site
n, err = t.Update(&o, z.Fields("name"), z.Where(z.Eq("id", id)))

Where

Example Description
Where("id=? and name=?", id, name) Regular formatted version
Where(Eq("id", id), Eq("name", name)...) Default to and connection
Where(And(Eq("x", x), Eq("y", y), Or(Eq("x", x), Eq("y", y)...)...) And & Or

Predefined Where Conditions

Name Example Description
Logical AND And(...) Any number of parameters, only accepts relational operators below
Logical OR Or(...) Any number of parameters, only accepts relational operators below
Normal condition Cond("id=?", id) Parameter 1 is formatted string, followed by placeholder parameters
Equal Eq("id", id) Two parameters, id=?
Not equal Neq("id", id) Two parameters, id<>?
Greater than Gt("id", id) Two parameters, id>?
Greater than or equal Gte("id", id) Two parameters, id>=?
Less than Lt("id", id) Two parameters, id<?
Less than or equal Lte("id", id) Two parameters, id<=?
Between Between("id", start, end) Three parameters, between start and end
Like Like("name", "x%") Two parameters, name like "x%"
GLOB GLOB("name", "?x*") Two parameters, name glob "?x*"
Multiple value selection In("id", ids) Two parameters, ids is basic type slice

GroupBy

Example Description
GroupBy("id", "name"...) -

Having

Example Description
Having("id=? and name=?", id, name) Regular formatted version
Having(Eq("id", id), Eq("name", name)...) Default to and connection
Having(And(Eq("x", x), Eq("y", y), Or(Eq("x", x), Eq("y", y)...)...) And & Or

OrderBy

Example Description
OrderBy("id desc", "name asc"...) -

Limit

Example Description
Limit(1) Page size 1
Limit(3, 2) Page size 3, offset position 2 (Note the difference from MySQL)

OnConflictDoUpdateSet

Example Description
OnConflictDoUpdateSet([]string{"id"}, V{"name": "new"}) Update to resolve primary key conflicts

Map Type Support

Example Description
Insert(map[string]interface{}{"name": "John", "age": 30}) Use map to insert data
Support all CRUD operations Select, Insert, Update, Delete all support map

Embedded Struct Support

Example Description
struct embeds other struct Automatically handle composite object fields
zorm:"-" tag Mark embedded struct

Field Ignore Functionality

Example Description
Password string zorm:"-" Ignore this field, not participate in database operations
Suitable for sensitive fields Such as passwords, temporary fields, etc.

IndexedBy

Example Description
IndexedBy("idx_biz_id") Solve index selectivity issues

How to Mock

Mock steps:

  • Call ZormMock to specify operations to mock
  • Use ZormMockFinish to check if mock was hit

Description:

  • First five parameters are tbl, fun, caller, file, pkg

    • Set to empty for default matching

    • Support wildcards '?' and '*', representing match one character and multiple characters respectively

    • Case insensitive

      Parameter Name Description
      tbl Table name Database table name
      fun Method name Select/Insert/Update/Delete
      caller Caller method name Need to include package name
      file File name File path where used
      pkg Package name Package name where used
  • Last three parameters are return data, return affected rows and error

  • Can only be used in test files

Usage example:

Function to test:

   package x

   func test(db *sql.DB) (X, int, error) {
      var o X
      tbl := z.Table(db, "tbl")
      n, err := tbl.Select(&o, z.Where("`id` >= ?", 1), z.Limit(100))
      return o, n, err
   }

In the x.test method querying tbl data, we need to mock database operations

   // Must set mock in _test.go file
   // Note caller method name needs to include package name
   z.ZormMock("tbl", "Select", "*.test", "", "", &o, 1, nil)

   // Call the function under test
   o1, n1, err := test(db)

   So(err, ShouldBeNil)
   So(n1, ShouldEqual, 1)
   So(o1, ShouldResemble, o)

   // Check if all hits
   err = z.ZormMockFinish()
   So(err, ShouldBeNil)

Performance Test Results

Reuse Function Performance Optimization

  • Benchmark Results:

    • Single thread: 8.6x performance improvement
    • Concurrent scenarios: Up to 14.2x performance improvement
    • Memory optimization: 92% memory usage reduction
    • Allocation optimization: 75% allocation count reduction
  • Technical Implementation:

    • Call site caching: Use runtime.Caller to cache file line numbers
    • String pooling: sync.Pool reuses strings.Builder
    • Zero allocation design: Avoid redundant string building and memory allocation
    • Concurrent safe: sync.Map supports high concurrency access
  • Performance Data:

    BenchmarkReuseOptimized-8    	 1000000	      1200 ns/op	     128 B/op	       2 allocs/op
    BenchmarkReuseOriginal-8     	  100000	     10320 ns/op	    1600 B/op	      15 allocs/op
    

Time Parsing Optimization

  • Before optimization: Using loop to try multiple time formats
  • After optimization: Smart format detection, single parse
  • Performance improvement: 5.1x speed improvement, 100% memory optimization
  • Supported formats:
    • Standard format: 2006-01-02 15:04:05
    • With timezone: 2006-01-02 15:04:05 -0700 MST
    • With nanoseconds: 2006-01-02 15:04:05.999999999 -0700 MST
    • Date only: 2006-01-02
    • Empty value handling: Automatically handle empty strings and NULL values

Field Cache Optimization

  • Technology: Use sync.Map to cache field mappings
  • Effect: Significantly improve performance for repeated operations
  • Applicable scenarios: Batch operations, frequent queries

String Operation Optimization

  • Optimization: Use strings.Builder instead of multiple string concatenations
  • Effect: Reduce memory allocation, improve string building performance

Reflection Optimization

  • Technology: Use reflect2 instead of standard reflect package
  • Effect: Zero use of ValueOf, avoid performance issues
  • Advantage: Faster type checking and field access

TODO

  • Insert/Update support non-pointer types
  • Transaction support
  • Join queries
  • Connection pool
  • Read-write separation

Sponsors

Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]

Contributors

The existence of this project is thanks to all contributors.

Please give us a 💖star💖 to support us, thank you.

And thank you to all our supporters! 🙏

About

Zima ORM library that is simple, ultra-fast and self-mockable for Go

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages