elorm

package module
v0.12.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2025 License: MIT Imports: 14 Imported by: 0

README

ELORM

The Golang ORM for crafting elegant, database-powered applications.

Go Reference Go Report Card

Ideas Under the Hood

Elorm implements a set of ideas from my business application engineering experience:

  • Globally unique, human-readable sequential ID for records/entities. This ID should be globally unique, sequential across all used databases, and should act as a URL for a particular entity. For better handling of database indexes, the ID should be sortable in order of creation, like autoincrement numbers.
  • Define all entities declaratively using a JSON entity declaration. elorm-gen processes it into strongly-typed structs and methods, ready to use in applications.
  • Shared parts of entities: fragments. Set of fields, indexes and event handlers. Each entity can include any count of fragments and inherit fields, indexes and event handlers.
  • Handle migrations automatically. It should be possible to upgrade from any version to any other version. Developers don't need to register each schema change as a separate migration. Of course, developers can run their own code as part of this migration. It works for both table and index definitions.
  • Core entity with basic functionality (loading, saving, caching, etc.). Application entities must be based on it.
  • Lazy-load navigation properties. It retrieves a referenced record on first access to the navigation property, from cache or from the database. You can have many navigation properties without impacting performance.
  • Global entity cache to track all loaded/created entities and reduce redundant queries to the database. Of course, you can tune cache size to balance between speed and memory for your application.
  • Use the standard database/sql to work with data. Engineers can use regular SQL select queries as well as specially designed methods.
  • Generate a standard REST API for each entity type. It should handle CRUD operations as well as grid/table operations (filtering, paging, sorting). Also, entities support JSON serialization out of the box.
  • Soft delete mode for entities allows us to transparently mark entities as deleted without deleting them from the database with minimum effort.
  • Optimistic locks allow a safe way for multi-user working with databases.

Quick start with ELORM

Define entities in JSON

We use standard json schema validation defined here: https://github.com/softilium/elorm-gen/blob/master/elorm.schema.json

Each entity consists of fields and indexes.

Also, you can define fragments: sets of fields and indexes to reuse between entities. Later you can define event handler routines for fragments as for entity types.

sample.schema.json (fragment)
{
    "PackageName": "main",
    "Fragments": [
        {
            "FragmentName": "BusinessObjects",
            "Columns": [
                {
                    "Name": "CreatedBy",
                    "ColumnType": "ref.User"
                },
                {
                    "Name": "CreatedAt",
                    "ColumnType": "datetime"
                },
                {
                    "Name": "ModifiedBy",
                    "ColumnType": "ref.User"
                },
                {
                    "Name": "ModifiedAt",
                    "ColumnType": "datetime"
                },
                {
                    "Name": "DeletedBy",
                    "ColumnType": "ref.User"
                },
                {
                    "Name": "DeletedAt",
                    "ColumnType": "datetime"
                }
            ]
        }
    ],
    "Entities": [
        {
            "ObjectName": "User",
            "TableName": "Users",
            "Indexes": [
                {
                    "Columns": [
                        "Username"
                    ],
                    "Unique": true
                },
                {
                    "Columns": [
                        "Email"
                    ],
                    "Unique": true
                }
            ],
            "Columns": [
                {
                    "Name": "Username",
                    "ColumnType": "string",
                    "Len": 100
                },
                {
                    "Name": "Email",
                    "ColumnType": "string",
                    "Len": 100
                },
                {
                    "Name": "PasswordHash",
                    "ColumnType": "string",
                    "Len": 200
                },
                {
                    "Name": "IsActive",
                    "ColumnType": "bool"
                },
                {
                    "Name": "ShopManager",
                    "ColumnType": "bool"
                },
                {
                    "Name": "Admin",
                    "ColumnType": "bool"
                },
                {
                    "Name": "TelegramUsername",
                    "ColumnType": "string",
                    "Len": 100
                },
                {
                    "Name": "TelegramCheckCode",
                    "ColumnType": "string",
                    "Len": 100
                },
                {
                    "Name": "TelegramVerified",
                    "ColumnType": "bool"
                },
                {
                    "Name": "TelegramChatId",
                    "ColumnType": "int"
                },
                {
                    "Name": "Description",
                    "ColumnType": "string",
                    "Len": 300
                }
            ]
        },
        {
            "ObjectName": "Token",
            "TableName": "Tokens",
            "Indexes": [
                {
                    "Columns": [
                        "User"
                    ],
                    "Unique": false
                },
                {
                    "Columns": [
                        "AccessToken",
                        "AccessTokenExpiresAt"
                    ],
                    "Unique": true
                },
                {
                    "Columns": [
                        "RefreshToken",
                        "RefreshTokenExpiresAt"
                    ],
                    "Unique": true
                }
            ],
            "Columns": [
                {
                    "Name": "User",
                    "ColumnType": "ref.User"
                },
                {
                    "Name": "AccessToken",
                    "ColumnType": "string",
                    "Len": 50
                },
                {
                    "Name": "AccessTokenExpiresAt",
                    "ColumnType": "datetime"
                },
                {
                    "Name": "RefreshToken",
                    "ColumnType": "string",
                    "Len": 50
                },
                {
                    "Name": "RefreshTokenExpiresAt",
                    "ColumnType": "datetime"
                }
            ]
        },
        {
            "ObjectName": "Shop",
            "TableName": "Shops",
            "Fragments": [
                "BusinessObjects"
            ],
            "Columns": [
                {
                    "Name": "Caption",
                    "ColumnType": "string",
                    "Len": 100
                },
                {
                    "Name": "Description",
                    "ColumnType": "string",
                    "Len": 300
                },
                {
                    "Name": "DeliveryConditions",
                    "ColumnType": "string",
                    "Len": 300
                }
            ]
        },
        {
            "ObjectName": "Good",
            "TableName": "Goods",
            "Fragments": [
                "BusinessObjects"
            ],
            "Indexes": [
                {
                    "Columns": [
                        "OwnerShop"
                    ],
                    "Unique": false
                }
            ],
            "Columns": [
                {
                    "Name": "OwnerShop",
                    "ColumnType": "ref.Shop"
                },
                {
                    "Name": "Caption",
                    "ColumnType": "string",
                    "Len": 100
                },
                {
                    "Name": "Article",
                    "ColumnType": "string",
                    "Len": 50
                },
                {
                    "Name": "Url",
                    "ColumnType": "string",
                    "Len": 500
                },
                {
                    "Name": "Description",
                    "ColumnType": "string",
                    "Len": 4096
                },
                {
                    "Name": "Price",
                    "ColumnType": "numeric",
                    "Precision": 10,
                    "Scale": 2
                },
                {
                    "Name": "OrderInShop",
                    "ColumnType": "int"
                }
            ]
        }
	}
}

Run elorm-gen to generate code

Install elorm-gen:

go get github.com/softilium/elorm-gen
go install github.com/softilium/elorm-gen

run elorm-gen for your json on your project folder:

elorm-gen sample.schema.json dbcontext-sample.go

dbcontext-sample.go should contain generated structs for your entities from sample.schema.json. It also contains methods for operating with data: loading, creating new entities, caching entities, and handling database structures.

By default generated file uses package name "main", but you can redefine it in JSON or on command line:

elorm-gen sample.schema.json dbcontext-sample.go --package m2
Initialize elorm on your app

Add "elorm" library to your project using:

go get github.com/softilium/elorm

Initialize db-context with your entities:


	dialect := "sqlite" // postgres, mssql, mysql also supported

	// syntax of connection string are defined by corresponding database driver
	connectionString := "file:todo.db?cache=shared"

	DB, err = CreateDbContext(dialect, connectionString)
	logError(err)

	// in case we have only one DB instance for database we can use aggressive caching strategy
	DB.AggressiveReadingCache = true

After initialized db-context can be enriched using additional event handlers. For example:

	err = DB.AddFillNewHandler(dbc.UserDef.EntityDef, func(entity any) error {
		user := entity.(*User)
		user.SetIsActive(true)
		return nil
	})
	if err != nil {
		return err
	}

Before first usage we need to ensure database has all tables and indexes ready to store data:

	err = DB.EnsureDBStructure()
	logError(err)
	fmt.Println("Database structure ensured successfully.")

After that your DB is ready to work with entitites.

Work with entities

Right after initialization DB could be used to process entities. Let's seed users table on first start and remove expired tokens:


	// create default admin on empty users table

	users, _, err := DB.UserDef.SelectEntities(nil, nil, 0, 0)
	if err != nil {
		logError(err)
	}
	if len(users) == 0 {

		admPassword := "<sample-password>"
		admUserName := "[email protected]"

		fmt.Println("No users found, creating default admin user...")
		adminUser, err := DB.CreateUser()
		if err != nil {
			logError(err)
		}
		admPwdHash, _ := HashPassword(admPassword)
		adminUser.SetEmail(admUserName)
		adminUser.SetUsername(admUserName)
		adminUser.SetPasswordHash(admPwdHash)
		adminUser.SetAdmin(true)
		err = adminUser.Save(context.Background())
		if err != nil {
			logError(err)
		}
	}

	// remove expired tokens on start
	tokensToDelete, _, err := DB.TokenDef.SelectEntities([]*elorm.Filter{
		elorm.AddFilterLT(DB.TokenDef.RefreshTokenExpiresAt, time.Now()),
	}, nil, 0, 0)
	if err != nil {
		logError(err)
	} else {
		for _, token := range tokensToDelete {
			err = DB.DeleteEntity(context.Background(), token.RefString())
			if err != nil {
				logError(err)
			}
		}
		fmt.Printf("Deleted %d expired tokens\n", len(tokensToDelete))
	}

Pay attention that all methods such as Save(), LoadEntity(), DeleteEntity() take ctx parameter to pass value to event handlers.

Create REST API from entities

ELORM allows you to create standard HTTP REST APIs for entities. Filtering, sorting and paging are supported out of the box. Let's look at an example:

	// first define config of Api element
	goodsRestApiConfig := elorm.CreateStdRestApiConfig(
		*DB.GoodDef.EntityDef,
		DB.LoadGood,
		DB.GoodDef.SelectEntities,
		DB.CreateGood)

	// define additional filter. This filter should always be merged with user-defined filters
	goodsRestApiConfig.AdditionalFilter = func(r *http.Request) ([]*elorm.Filter, error) {
		res := []*elorm.Filter{}
		res = append(res, elorm.AddFilterGT(DB.GoodDef.Price 10))
		shopref := r.URL.Query().Get("shopref")
		if shopref != "" {
			res = append(res, elorm.AddFilterEQ(DB.GoodDef.OwnerShop, shopref))
		}
		return res, nil
	}
	// define default sort order. User can define it's own using query parameter
	goodsRestApiConfig.DefaultSorts = func(r *http.Request) ([]*elorm.SortItem, error) {
		return []*elorm.SortItem{{Field: DB.GoodDef.OrderInShop, Asc: true}}, nil
	}
	// Context func creates all needed values to pass to event handlers
	goodsRestApiConfig.Context = LoadUserFromHttpToContext

	//here we connect handler to end-point on standard http router 
	router.HandleFunc("/api/goods", elorm.HandleRestApi(goodsRestApiConfig))

See more about RestApiConfig: https://pkg.go.dev/github.com/softilium/elorm#RestApiConfig

Soft delete for entities

Entity definition supports UseSoftDelete mode. By default, UseSoftDelete is false.

In this mode ELORM adds filter "IsDeleted=true" to SelectEntities() filter unless developer adds his own filter on "IsDeleted". Also REST API handles DELETE requests as "let's mark entity as IsDelete=true" instead of deleting it.

In default mode (UseSoftDelete=false) Save() method returns an error is we set IsDeleted to true.

Note. Each entity has IsDeleted field, UseSoftDelete=false doesn't remove the field.

Use standard Go idiomatic approaches

ELORM stay on top on best Go idiomatic code approaches when it is possible and as many as it possible.

database/sql

Developers can use standard "database/sql" approach to retrieve data and ELORM can enrich Scanner interface:

	rows, err := DB.Query("select ref from goods order by ref")
	checkErr(err)
	defer rows.Close()
	idx := 0
	for rows.Next() {

        var ref string
        rows.Scan(&ref)
		checkErr(err)

		// access to row column as typed Entity
		loadedGood, err := DB.LoadGood(ref)
		checkErr(err)

		// access to lazy-loading property CreatedBy() (User) and its property Username
		fmt.Println(loadedGood.CreatedBy().Username())
	}
context

Standard Go context is used to pass any additional parameters to event handlers:

	// let's define before save handler for any entity with reference to BusinessObject fragment (Shop, Good)
	err := DB.AddBeforeSaveHandler(BusinessObjectsFragment, func(ctx context.Context, entity any) error {
		user, ok := ctx.Value(userContextKey).(*User)
		if !ok {
			user = nil
		}
		et := entity.(BusinessObjectsFragmentMethods)
		ent := entity.(elorm.IEntity)
		if ent.IsNew() {
			et.SetCreatedAt(time.Now())
			if user != nil {
				et.SetCreatedBy(user)
			}
			if et.CreatedBy() == nil {
				return fmt.Errorf("createdBy is required for new entity %s", ent.Def().ObjectName)
			}
		} else {
			et.SetModifiedAt(time.Now())
			if user != nil {
				et.SetModifiedBy(user)
			}
		}
	})

	AddUserContext := func (ctx context.Context, user *User) context.Context {
		if user != nil {
			ctx = context.WithValue(ctx, userContextKey, user)
		}
		return ctx
	}

	// get first user as Current from database
	CurrentUser := DB.UserDef.SelectEntities(nil, nil, 1, 1)
	saveCtx := AddUserContext(context.Background())
	NewGood, err := DB.CreateGood()
	if err != nil {
		logError(err)
	}

	//save new Good, NewGood.CreatedBy should be initialized by CurrentUser via event handler
	err = NewGood.Save(saveCtx)
	if err != nil {
		logError(err)
	}

JSON marshaling/unmarchaling

By default, any entity can be serialized to JSON and deserialized from JSON. Standard Marshaler/Unmarshaler interfaces are implemented.

Occasionally, we need to "expand" some references (navigation properties). For example, it is a common approach to serialize user not just as user ID but as object with base properties: refID, userName, email, etc.

Elorm supports automatic references expanding for JSON when you need it. Use AutoExpandFieldsForJSON property for Entity Definition:

	dbc.UserDef.AutoExpandFieldsForJSON = map[*elorm.FieldDef]bool{
		dbc.UserDef.Ref:      true,
		dbc.UserDef.Username: true,
	}

After that all fields that reference User should be expanded to Ref, Username when you serialize entity to JSON.

DataVersion checking and AggressiveCaching

We use typical optimistic locks implementation in ELORM. DataVersion field is assigned to new value before each save. And if it is defined on factory and entity def levels, we check that actual database version contains old value of DataVersion field. If it contains a different value, it means another client has updated row in database after we read it last time. In that case Save() returns an error.

Work with old field values

When we load entity and change values for some fields then old values are accessible via Old() methods before we saved entity to database. It is useful to analyze changes in BeforeSave handlers.

Cache entities, lazy-loading reference (navigation) properties

All loaded entities are cached at the factory level using LRU cache. Next LoadEntity() will load entity from cache instead of querying the database. Our internal tests show that it increases speed of loading entities by about 100 times. Developers don't need to worry about cache or do anything to maintain it.

Handling Transactions

Factory struct created and holds database connector (*sql.DB) based on dialect and connection string parameters.

ELORM handles transactions on all standard operations such as save or delete. BeforeSave event handler works within main transaction and when handler returns an error, save transactions will be rolled back.

AfterSave event handler works after main transaction is committed.

Developers don't need to start a transaction before saving or deleting entities. But when you need a transaction to wrap some actions into it, the recommended approach is:

		tx, err := DB.BeginTran()
		if err != nil {
			HandleErr(err)
			return
		}
		defer func() { _ = DB.RollbackTran(tx) }()

		old, _, err := DB.GoodTagDef.SelectEntities(
			[]*elorm.Filter{elorm.AddFilterEQ(DB.GoodTagDef.Good, good)}, nil, 0, 0)
		if err != nil {
			HandleErr(err)
			return
		}
		for _, ot := range old {
			err = DB.DeleteEntity(r.Context(), ot.RefString())
			if err != nil {
				HandleErr(err)
				return
			}
		}
		for _, line := range result {
			if line.Tagged {
				gt, err := DB.CreateGoodTag()
				gt.SetGood(good)
				gt.SetTag(tg)
				err = gt.Save(r.Context())
				if err != nil {
					HandleErr(err))
					return
				}
			}
		}
		err = DB.CommitTran(tx)
		if err != nil {
			HandleErr(err))
			return
		}
SQLite and multithreading

Let's use simple example to explain.

So, we have two entity types: Order and OrderLine. Order owns some Order lines and order lines should be deleted before order is deleted. Typically, we implement BeforeDelete handler for Order to delete OrderLines. Each DeleteEntity has its own transaction and we want to rollback all transactions when any is rolled back. For most databases it is implemented by different *sql.Tx transactions. Except SQLite.

SQLite doesn't support more than one writing transaction at the same time. Because of this we use different approach for "nested" transactions on SQLite. We start first transaction as usual. But second transaction doesn't really start on SQLite. We just increase transaction level counter. When we commit transaction we decrease that counter. If counter goes to zero, we issue "Commit" to SQLite. It allows us to emulate "nested" transactions behavior for SQLite.

Dark side of this solution is that we need to stay away from goroutines when we use SQLite. One goroutine can break "transaction counter" for another goroutine.

We fully support goroutines and multithreading in ELORM for MySQL, Postgres and Microsoft SQL databases.

Fragments

Fragments are just sets of columns, indexes and event handlers. Fragment is defined in JSON using similar way as entity. Later, entities can reference a fragment and all columns from fragment should be copied to entity. We can describe some set of "functionality" and then reuse it many times in many entities. This makes our code compact and easy to understand.

Note, we still use strongly-typed entities and when we remove field from fragment definition, compiler will show all removed field usage lines.

Wrapping entities into strongly-typed structs with methods

Each entity type definition can assign special function that converts "universal" entity into strongly typed one. Strongly typed entity can define methods to access fields, additional service methods, etc. elorm-gen uses this functionality to automatically create strongly typed entities for you based on JSON entity definitions.

Documentation

Index

Constants

View Source
const (
	RefFieldName         = "Ref"
	IsDeletedFieldName   = "IsDeleted"
	DataVersionFieldName = "DataVersion"
)

Predefined field names for common fields in entities.

View Source
const (
	DbDialectPostgres = 100
	DbDialectMSSQL    = 200
	DbDialectMySQL    = 300
	DbDialectSQLite   = 400
)

Database dialect constants for supported database types.

View Source
const (
	DataVersionCheckNever   = -1
	DataVersionCheckDefault = 0
	DataVersionCheckAlways  = 1
)

Data version check mode constants for controlling version checking behavior.

View Source
const (
	FieldDefTypeString   = 100
	FieldDefTypeInt      = 200
	FieldDefTypeBool     = 300
	FieldDefTypeRef      = 400
	FieldDefTypeNumeric  = 500
	FieldDefTypeDateTime = 600
)

Supported field types

View Source
const (
	FilterEQ        = 50
	FilterLIKE      = 55
	FilterNOEQ      = 60
	FilterGT        = 70
	FilterGE        = 80
	FilterLT        = 90
	FilterLE        = 100
	FilterIN        = 110
	FilterNOTIN     = 120
	FilterIsNULL    = 130
	FilterIsNOTNULL = 140
	FilterAndGroup  = 200
	FilterOrGroup   = 210
)

Filter operation constants for entity filtering in SelectEntities.

View Source
const DefaultPageSize = 20

DefaultPageSize is the default number of items per page in REST API responses.

Variables

This section is empty.

Functions

func HandleRestApi

func HandleRestApi[T IEntity](config RestApiConfig[T]) func(w http.ResponseWriter, r *http.Request)

HandleRestApi handles HTTP requests for the REST API based on the provided configuration.

func NewRef

func NewRef() string

NewRef generates a new unique ID/URL for entities and other purposes.

Types

type Entity

type Entity struct {
	Factory *Factory

	Values map[string]IFieldValue
	// contains filtered or unexported fields
}

Entity represents a database entity in elorm.

func (*Entity) DataVersion

func (T *Entity) DataVersion() string

DataVersion returns the current data version of this entity.

func (*Entity) Def added in v0.4.0

func (T *Entity) Def() *EntityDef

Def returns the entity definition for this entity.

func (*Entity) GetValues added in v0.4.0

func (T *Entity) GetValues() map[string]IFieldValue

GetValues returns a map of all field values for this entity.

func (*Entity) IsDeleted

func (T *Entity) IsDeleted() bool

IsDeleted returns true if this entity is marked for deletion.

func (*Entity) IsNew

func (T *Entity) IsNew() bool

IsNew returns true if this entity has not been saved to the database yet.

func (*Entity) LoadFrom

func (T *Entity) LoadFrom(src IEntity, predefinedFields bool) error

LoadFrom copies field values from another entity into this entity.

func (*Entity) MarshalJSON

func (T *Entity) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler interface for JSON serialization.

func (*Entity) RefString

func (T *Entity) RefString() string

RefString returns the string representation of this entity's reference.

func (*Entity) Save

func (T *Entity) Save(ctx context.Context) error

Save persists the Entity to the database. It handles both insert and update operations depending on whether the entity is new or existing. The method performs the following steps:

  • Executes the BeforeSaveHandler if defined.
  • Begins a database transaction.
  • If the entity is new, inserts a new record into the database.
  • If the entity exists, updates the corresponding record, optionally performing data version checks to prevent concurrent modifications.
  • Commits the transaction if all operations succeed, or rolls back on error.
  • Executes the AfterSaveHandler if defined.

Returns an error if any step fails, including handler execution, SQL operations, or transaction management.

func (*Entity) SetIsDeleted

func (T *Entity) SetIsDeleted(newValue bool)

SetIsDeleted sets the deletion status of this entity.

func (*Entity) UnmarshalJSON

func (T *Entity) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler interface for JSON deserialization.

type EntityDef

type EntityDef struct {
	Factory                 *Factory                 // Owning factory, used to access database and other resources
	DataVersionCheckMode    int                      // DataVersionCheckNever, DataVersionCheckDefault, DataVersionCheckAlways
	ObjectName              string                   // name of the object, elorm-gen created strongly typed structs based on this name
	TableName               string                   // SQL table name, if empty, it will be generated from ObjectName
	Fragments               []string                 // fragments are used to define reusable parts of entity definitions
	FieldDefs               []*FieldDef              // defined fields. All predefined fields (Ref, IsDeleted, DataVersion) are automatically added to this list.
	IndexDefs               []*IndexDef              // defined indexes. PK doesn't need to be defined here, it is always created automatically
	RefField                *FieldDef                // Primary Key, ID
	IsDeletedField          *FieldDef                // field for soft delete
	DataVersionField        *FieldDef                // field for data versioning
	Wrap                    func(source *Entity) any // optional function to wrap the entity type into custom struct (used by elorm-gen)
	AutoExpandFieldsForJSON map[*FieldDef]bool       // if specified, these fields will be automatically expanded when serializing to JSON

	// UseSoftDelete=true leads to:
	// 1) SeletecEntities() includes IsDeleted=false filter always unless developer specified it explicitly
	// 2) HandleRestApi on DELETE requests will set IsDeleted=true instead of deleting the entity
	// Note: entity has IsDelete field always, developer can use always
	//
	// UseSoftDelete=false leads to:
	// 1) Save() raises error if IsDeleted=true and UseSoftDelete=false
	UseSoftDelete bool
	// contains filtered or unexported fields
}

EntityDef describes the definition of an entity.

func (*EntityDef) ActualDataVersionCheckMode added in v0.6.0

func (T *EntityDef) ActualDataVersionCheckMode() int

ActualDataVersionCheckMode returns the effective data version check mode for this entity.

func (*EntityDef) AddBoolFieldDef

func (T *EntityDef) AddBoolFieldDef(name string) (*FieldDef, error)

AddBoolFieldDef adds a boolean field definition to this entity def.

func (*EntityDef) AddDateTimeFieldDef

func (T *EntityDef) AddDateTimeFieldDef(name string) (*FieldDef, error)

AddDateTimeFieldDef adds a datetime field definition to this entity def.

func (*EntityDef) AddIndex added in v0.3.0

func (T *EntityDef) AddIndex(Unique bool, fld ...*FieldDef) error

AddIndex adds a new index to the entity definition. The index can be unique or non-unique, and is defined over one or more fields. Index will be created or updated automatically in scope of ensureDatabaseIndexes() method. Parameters:

  • Unique: specifies whether the index should enforce uniqueness.
  • fld: variadic list of FieldDef representing the fields to include in the index.

Returns:

  • error: describing the reason for failure, or nil if the index was added successfully.

func (*EntityDef) AddIntFieldDef

func (T *EntityDef) AddIntFieldDef(name string) (*FieldDef, error)

AddIntFieldDef adds an integer field definition to this entity def.

func (*EntityDef) AddNumericFieldDef

func (T *EntityDef) AddNumericFieldDef(name string, Precision int, Scale int) (*FieldDef, error)

AddNumericFieldDef adds a numeric field definition to this entity def.

func (*EntityDef) AddRefFieldDef

func (T *EntityDef) AddRefFieldDef(name string, refType *EntityDef) (*FieldDef, error)

AddRefFieldDef adds a reference field definition to this entity def.

func (*EntityDef) AddStringFieldDef

func (T *EntityDef) AddStringFieldDef(name string, size int) (*FieldDef, error)

AddStringFieldDef adds a string field definition to this entity def.

func (*EntityDef) FieldDefByName

func (T *EntityDef) FieldDefByName(name string) *FieldDef

FieldDefByName returns the field definition with the specified name.

func (*EntityDef) SelectEntities

func (T *EntityDef) SelectEntities(filters []*Filter, sorts []*SortItem, pageNo int, pageSize int) (result []*Entity, pagesCount int, err error)

SelectEntities retrieves entities from the database with filtering, sorting, and pagination.

func (*EntityDef) SqlTableName

func (T *EntityDef) SqlTableName() (string, error)

SqlTableName returns the SQL table name for this entity definition.

type EntityHandlerFunc

type EntityHandlerFunc func(ctx context.Context, entity any) error

EntityHandlerFunc is a function type for handling entities with context. Used for beforeSave, afterSave, beforeDelete handlers.

type EntityHandlerFuncByRef added in v0.2.0

type EntityHandlerFuncByRef func(ctx context.Context, ref string) error

EntityHandlerFuncByRef is a function type for handling entities by reference with context. Entity is not instantiated; only reference is provided.

type EntityHandlerFuncNoContext added in v0.4.0

type EntityHandlerFuncNoContext func(entity any) error

EntityHandlerFuncNoContext is a function type for handling entities without context. Used for fillNewHandlers.

type Factory

type Factory struct {
	AggressiveReadingCache bool // It assumes each database has only one factory instance, so it can cache entities aggressively.
	EntityDefs             []*EntityDef
	// contains filtered or unexported fields
}

Factory manages entities, keeps database connections, entities cache, and handles transactions.

func CreateFactory

func CreateFactory(dbDialect string, connectionString string) (*Factory, error)

CreateFactory creates a new Factory instance with the specified database dialect and connection string.

func (*Factory) AddAfterSaveHandler added in v0.2.0

func (f *Factory) AddAfterSaveHandler(dest any, handler EntityHandlerFunc) error

AddAfterSaveHandler adds a handler that will be called after saving entities. dest

func (*Factory) AddBeforeDeleteHandler added in v0.2.0

func (f *Factory) AddBeforeDeleteHandler(dest any, handler EntityHandlerFunc) error

AddBeforeDeleteHandler adds a handler that will be called before deleting entities. dest can be an EntityDef pointer or a fragment name.

func (*Factory) AddBeforeDeleteHandlerByRef added in v0.2.0

func (f *Factory) AddBeforeDeleteHandlerByRef(dest any, handler EntityHandlerFuncByRef) error

AddBeforeDeleteHandlerByRef adds a handler that will be called before deleting entities by reference. dest

func (*Factory) AddBeforeSaveHandler added in v0.2.0

func (f *Factory) AddBeforeSaveHandler(dest any, handler EntityHandlerFunc) error

AddBeforeSaveHandler adds a handler that will be called before saving entities. dest can be an EntityDef pointer or a fragment name.

func (*Factory) AddBeforeSaveHandlerByRef added in v0.2.0

func (f *Factory) AddBeforeSaveHandlerByRef(dest any, handler EntityHandlerFuncByRef) error

AddBeforeSaveHandlerByRef adds a handler that will be called before saving entities by reference. dest can be an EntityDef pointer or a fragment name.

func (*Factory) AddFillNewHandler added in v0.2.0

func (f *Factory) AddFillNewHandler(dest any, handler EntityHandlerFuncNoContext) error

AddFillNewHandler adds a handler that will be called when creating new entities. dest can be an EntityDef pointer or a fragment name.

func (*Factory) BeginTran

func (f *Factory) BeginTran() (*sql.Tx, error)

BeginTran begins a database transaction or increases the nested transaction level if already in a transaction.

func (*Factory) CommitTran

func (f *Factory) CommitTran(tx *sql.Tx) error

CommitTran decreases transaction level and commits the transaction if it was the last one.

func (*Factory) CreateEntity

func (T *Factory) CreateEntity(def *EntityDef) (*Entity, error)

CreateEntity creates a new entity instance with the given entity definition.

func (*Factory) CreateEntityDef

func (T *Factory) CreateEntityDef(ObjectName string, TableName string) (*EntityDef, error)

CreateEntityDef creates a new entity definition with the specified object and table names.

func (*Factory) CreateEntityWrapped

func (T *Factory) CreateEntityWrapped(def *EntityDef) (any, error)

CreateEntityWrapped creates a new entity instance wrapped in a custom struct if defined.

func (*Factory) DbDialect

func (T *Factory) DbDialect() int

DbDialect returns the database dialect for this factory.

func (*Factory) DeleteEntity

func (T *Factory) DeleteEntity(ctx context.Context, ref string) error

func (*Factory) EnsureDBStructure

func (T *Factory) EnsureDBStructure() error

func (*Factory) Exec added in v0.3.0

func (f *Factory) Exec(query string, args ...any) (sql.Result, error)

Exec executes a query without returning any rows.

func (*Factory) IsRef

func (T *Factory) IsRef(s string) (bool, *EntityDef)

IsRef checks if the given string is a valid reference and returns the associated entity definition.

func (*Factory) LoadEntity

func (T *Factory) LoadEntity(Ref string) (*Entity, error)

LoadEntity loads an entity from factory cache or from the database by its reference string.

func (*Factory) LoadEntityWrapped

func (T *Factory) LoadEntityWrapped(Ref string) (any, error)

LoadEntityWrapped loads an entity from the database and wraps it in a custom struct if defined.

func (*Factory) NewRef

func (T *Factory) NewRef(def *EntityDef) string

NewRef generates a new reference string for the given entity definition. It will include def ObjectName if provided.

func (*Factory) PrepareSql added in v0.9.2

func (f *Factory) PrepareSql(query string, args ...any) string

PrepareSql prepares a SQL query by replacing Postgres-style parameters ($1, $2, ...) with MySQL/SQLite-style (?) if needed.

func (*Factory) Query

func (f *Factory) Query(query string, args ...any) (*sql.Rows, error)

Query is a wrapper for sql.DB.Query(). It always accepts parameters in Postgres style ($1, $2, ...) and converts it to MySQL/SQLite style (?) if needed. And it uses database connection or active transaction depending on the dialect and transaction level.

func (*Factory) RollbackTran

func (f *Factory) RollbackTran(tx *sql.Tx) error

RollbackTran rolls back a database transaction and zeroes the transaction level.

func (*Factory) SetDataVersionCheckMode

func (T *Factory) SetDataVersionCheckMode(mode int) error

SetDataVersionCheckMode sets default data version checking mode for this factory. It can be overridden by EntityDef level.

type FieldDef

type FieldDef struct {
	Name string
	Type int

	EntityDef          *EntityDef // for ref fields, the entity definition this field navigate
	Len                int        //for string
	Precision          int        //for numeric
	Scale              int        //for numeric
	DateTimeJSONFormat string     //for date time, e.g. "2006-01-02T15:04:05Z07:00"
}

FieldDef describes a field in an entity.

func (*FieldDef) CreateFieldValue

func (T *FieldDef) CreateFieldValue(entity *Entity) (IFieldValue, error)

func (*FieldDef) SqlColumnName

func (T *FieldDef) SqlColumnName() (string, error)

func (*FieldDef) SqlColumnType

func (T *FieldDef) SqlColumnType() (string, error)

type FieldValueBool

type FieldValueBool struct {
	// contains filtered or unexported fields
}

FieldValueBool is the bool field value implementation.

func (*FieldValueBool) AsString

func (T *FieldValueBool) AsString() string

func (*FieldValueBool) Def

func (T *FieldValueBool) Def() *FieldDef

func (*FieldValueBool) Entity

func (T *FieldValueBool) Entity() *Entity

func (*FieldValueBool) Get

func (T *FieldValueBool) Get() bool

func (*FieldValueBool) Old added in v0.10.0

func (T *FieldValueBool) Old() bool

func (*FieldValueBool) Scan

func (T *FieldValueBool) Scan(v any) error

func (*FieldValueBool) Set

func (T *FieldValueBool) Set(newValue bool)

func (*FieldValueBool) SqlStringValue

func (T *FieldValueBool) SqlStringValue(v ...any) (string, error)

type FieldValueDateTime

type FieldValueDateTime struct {
	// contains filtered or unexported fields
}

FieldValueDateTime is the datetime field value implementation that stores date and time values without timezone.

func (*FieldValueDateTime) AsString

func (T *FieldValueDateTime) AsString() string

func (*FieldValueDateTime) Def

func (T *FieldValueDateTime) Def() *FieldDef

func (*FieldValueDateTime) Entity

func (T *FieldValueDateTime) Entity() *Entity

func (*FieldValueDateTime) Get

func (T *FieldValueDateTime) Get() time.Time

func (*FieldValueDateTime) Old added in v0.10.0

func (T *FieldValueDateTime) Old() time.Time

func (*FieldValueDateTime) Scan

func (T *FieldValueDateTime) Scan(v any) error

func (*FieldValueDateTime) Set

func (T *FieldValueDateTime) Set(newValue time.Time)

func (*FieldValueDateTime) SqlStringValue

func (T *FieldValueDateTime) SqlStringValue(v ...any) (string, error)

type FieldValueInt

type FieldValueInt struct {
	// contains filtered or unexported fields
}

FieldValueInt is the int64 field value implementation.

func (*FieldValueInt) AsString

func (T *FieldValueInt) AsString() string

func (*FieldValueInt) Def

func (T *FieldValueInt) Def() *FieldDef

func (*FieldValueInt) Entity

func (T *FieldValueInt) Entity() *Entity

func (*FieldValueInt) Get

func (T *FieldValueInt) Get() int64

func (*FieldValueInt) Old added in v0.10.0

func (T *FieldValueInt) Old() int64

func (*FieldValueInt) Scan

func (T *FieldValueInt) Scan(v any) error

func (*FieldValueInt) Set

func (T *FieldValueInt) Set(newValue int64)

func (*FieldValueInt) SqlStringValue

func (T *FieldValueInt) SqlStringValue(v ...any) (string, error)

type FieldValueNumeric

type FieldValueNumeric struct {
	// contains filtered or unexported fields
}

FieldValueNumeric is the float64 field value implementation.

func (*FieldValueNumeric) AsString

func (T *FieldValueNumeric) AsString() string

func (*FieldValueNumeric) Def

func (T *FieldValueNumeric) Def() *FieldDef

func (*FieldValueNumeric) Entity

func (T *FieldValueNumeric) Entity() *Entity

func (*FieldValueNumeric) Get

func (T *FieldValueNumeric) Get() float64

func (*FieldValueNumeric) Old added in v0.10.0

func (T *FieldValueNumeric) Old() float64

func (*FieldValueNumeric) Scan

func (T *FieldValueNumeric) Scan(v any) error

func (*FieldValueNumeric) Set

func (T *FieldValueNumeric) Set(newValue float64)

func (*FieldValueNumeric) SqlStringValue

func (T *FieldValueNumeric) SqlStringValue(v ...any) (string, error)

type FieldValueRef

type FieldValueRef struct {
	// contains filtered or unexported fields
}

FieldValueRef is the reference (navigation) field value implementation. It supports lazy loading of referenced entity.

func (*FieldValueRef) AsString

func (T *FieldValueRef) AsString() string

func (*FieldValueRef) Def

func (T *FieldValueRef) Def() *FieldDef

func (*FieldValueRef) Entity

func (T *FieldValueRef) Entity() *Entity

func (*FieldValueRef) Get

func (T *FieldValueRef) Get() (any, error)

func (*FieldValueRef) Old added in v0.10.0

func (T *FieldValueRef) Old() (any, error)

func (*FieldValueRef) Scan

func (T *FieldValueRef) Scan(v any) error

func (*FieldValueRef) Set

func (T *FieldValueRef) Set(newValue any) error

func (*FieldValueRef) SetFactory

func (T *FieldValueRef) SetFactory(newValue *Factory)

func (*FieldValueRef) SqlStringValue

func (T *FieldValueRef) SqlStringValue(v ...any) (string, error)

type FieldValueString

type FieldValueString struct {
	// contains filtered or unexported fields
}

FieldValueString is the string field value implementation.

func (*FieldValueString) AsString

func (T *FieldValueString) AsString() string

func (*FieldValueString) Def

func (T *FieldValueString) Def() *FieldDef

func (*FieldValueString) Entity

func (T *FieldValueString) Entity() *Entity

func (*FieldValueString) Get

func (T *FieldValueString) Get() string

func (*FieldValueString) Old added in v0.10.0

func (T *FieldValueString) Old() string

func (*FieldValueString) Scan

func (T *FieldValueString) Scan(v any) error

func (*FieldValueString) Set

func (T *FieldValueString) Set(newValue string)

func (*FieldValueString) SqlStringValue

func (T *FieldValueString) SqlStringValue(v ...any) (string, error)

type Filter

type Filter struct {
	Op      int
	LeftOp  *FieldDef
	RightOp any
	Childs  []*Filter
}

Filter represents a filter condition for entity selection (SelectEntities).

func AddAndGroup

func AddAndGroup(childs ...*Filter) *Filter

AddAndGroup creates a filter group with AND logic for combining multiple filters.

func AddFilterEQ

func AddFilterEQ(leftField *FieldDef, rightValue any) *Filter

AddFilterEQ creates a filter for equality comparison.

func AddFilterGE

func AddFilterGE(leftField *FieldDef, rightValue any) *Filter

AddFilterGE creates a filter for greater than or equal comparison.

func AddFilterGT

func AddFilterGT(leftField *FieldDef, rightValue any) *Filter

AddFilterGT creates a filter for greater than comparison.

func AddFilterIN added in v0.6.0

func AddFilterIN(leftField *FieldDef, rightValues ...any) *Filter

AddFilterIN creates a filter for IN comparison with multiple values.

func AddFilterIsNOTNULL added in v0.9.0

func AddFilterIsNOTNULL(leftField *FieldDef) *Filter

AddFilterIsNOTNULL creates a filter for NOT NULL value check.

func AddFilterIsNULL added in v0.6.0

func AddFilterIsNULL(leftField *FieldDef) *Filter

AddFilterIsNULL creates a filter for NULL value check.

func AddFilterLE

func AddFilterLE(leftField *FieldDef, rightValue any) *Filter

AddFilterLE creates a filter for less than or equal comparison.

func AddFilterLIKE

func AddFilterLIKE(leftField *FieldDef, rightValue string) *Filter

AddFilterLIKE creates a filter for LIKE string comparison.

func AddFilterLT

func AddFilterLT(leftField *FieldDef, rightValue any) *Filter

AddFilterLT creates a filter for less than comparison.

func AddFilterNOEQ

func AddFilterNOEQ(leftField *FieldDef, rightValue any) *Filter

AddFilterNOEQ creates a filter for inequality comparison.

func AddFilterNOTIN added in v0.6.0

func AddFilterNOTIN(leftField *FieldDef, rightValues ...any) *Filter

AddFilterNOTIN creates a filter for NOT IN comparison with multiple values.

func AddOrGroup

func AddOrGroup(childs ...*Filter) *Filter

AddOrGroup creates a filter group with OR logic for combining multiple filters.

type IEntity

type IEntity interface {
	RefString() string
	IsNew() bool
	IsDeleted() bool
	Save(ctx context.Context) error
	LoadFrom(src IEntity, predefinedFields bool) error
	GetValues() map[string]IFieldValue
	Def() *EntityDef
}

IEntity is the interface for entities in elorm.

type IFieldValue

type IFieldValue interface {
	Def() *FieldDef
	Entity() *Entity
	SqlStringValue(v ...any) (string, error)
	Scan(v any) error
	AsString() string
	// contains filtered or unexported methods
}

IFieldValue is the interface for field values in elorm.

type IndexDef

type IndexDef struct {
	Unique    bool
	FieldDefs []*FieldDef
}

IndexDef represents an index definition for an Entity struct.

type RestApiConfig

type RestApiConfig[T IEntity] struct {

	// Entity Definition
	Def *EntityDef

	// Typically, it's LoadXXX from generated code, e.g. LoadShop
	LoadEntityFunc func(ref string) (T, error)

	// Typically, it's SelectEntities from generated code, e.g. DB.ShopDef.SelectEntities
	SelectEntitiesFunc func(filters []*Filter, sorts []*SortItem, pageNo int, pageSize int) (result []T, pagesCount int, err error)

	// Typically, it's CreateXXX from generated code, e.g. DB.CreateShop
	CreateEntityFunc func() (T, error)

	// Additional headers to include into the response
	AdditionalHeaders map[string]string

	// Automatically merge filters based on query parameters
	AutoFilters bool

	// Default number of items per page in REST API responses
	DefaultPageSize int

	// Enable GET request for a single entity
	EnableGetOne bool

	// Enable GET request for a list of entities
	EnableGetList bool

	// Enable POST request to create a new entity
	EnablePost bool

	// Enable PUT request to update an existing entity
	EnablePut bool

	// Enable DELETE request to remove an entity
	EnableDelete bool

	// Query parameter name for entity reference, "ref" by default
	ParamRef string

	// Query parameter name for page number, "pageno" by default
	ParamPageNo string

	// Query parameter name for page size, "pagesize" by default
	ParamPageSize string

	// Query parameter name for sorting, "sortby" by default
	ParamSortBy string

	// Middleware function to execute before processing the request, returns true to continue or false to stop
	BeforeMiddleware func(http.ResponseWriter, *http.Request) bool

	// Function to get context from the request, can be used to pass additional values to event handlers
	Context func(r *http.Request) context.Context

	// Function to get additional filters based on the request, result should be merged with user-defined filters
	AdditionalFilter func(r *http.Request) ([]*Filter, error)

	// Function to get default sorting options based on the request, result is used when we have no user-defined sorts. Should be merged with user-defined sorts
	DefaultSorts func(r *http.Request) ([]*SortItem, error)
}

RestApiConfig is a configuration for standard REST API operations.

func CreateStdRestApiConfig

func CreateStdRestApiConfig[T IEntity](
	def *EntityDef,
	loadEntityFunc func(ref string) (T, error),
	selectEntitiesFunc func(filters []*Filter, sorts []*SortItem, pageNo int, pageSize int) (result []T, pages int, err error),
	createEntityFunc func() (T, error)) RestApiConfig[T]

CreateStdRestApiConfig creates a new RestApiConfig for standard REST API operations.

type SortItem

type SortItem struct {
	Field *FieldDef
	Asc   bool
}

SortItem represents a sort condition element for SelectEntities

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL