ORM (Object-Relational Mapping)

Object-Relational Mapping #

The INFINI Framework provides a powerful ORM system built on top of Elasticsearch(Including OpenSearch,Easysearch support), enabling developers to define, store, and query structured data objects with ease. The ORM handles object mapping, indexing, and provides a comprehensive set of CRUD operations.

Object Definition #

Objects in the ORM are defined by embedding orm.ORMObjectBase in your struct, which automatically provides all required ORM functionality including system fields for metadata management. Objects must also implement the Object interface with GetID() and SetID(string) methods.

Key Principle: ORMObjectBase Inheritance #

To make any struct ORM-capable, you must embed orm.ORMObjectBase as the first field. This provides:

  • System fields for metadata management
  • Required interface implementations
  • Automatic timestamp handling
  • Built-in ID management

Basic Object Structure #

// Basic ORM object with ORMObjectBase inheritance
type User struct {
    // EMBED ORMObjectBase as the first field - REQUIRED
    orm.ORMObjectBase        // Embedding ORM base for persistence-related fields

    // Custom fields
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Age       int       `json:"age"`
}

// Implement required Object interface methods (ORMObjectBase already handles GetID/SetID)

Nested Inheritance: Building on Base Objects #

The ORM supports nested inheritance, allowing you to create reusable base objects and extend them:

// Define a base object with shared functionality
type CombinedFullText struct {
    orm.ORMObjectBase        // Embedding ORM base for persistence-related fields
    CombinedFullText  string `json:"-" elastic_mapping:"combined_fulltext:{type:text,index_prefixes:{},index_phrases:true,analyzer:combined_text_analyzer }"`

    Metadata map[string]interface{} `json:"metadata,omitempty" elastic_mapping:"metadata:{type:object}"` // Additional accessible metadata
    Payload  map[string]interface{} `json:"payload,omitempty" elastic_mapping:"payload:{enabled:false}"` // Store-only metadata
}

// Extend the base object with specific fields
type DataSource struct {
    CombinedFullText  // Inherit all fields from base object

    Type        string `json:"type,omitempty" elastic_mapping:"type:{type:keyword,copy_to:combined_fulltext}"`
    Name        string `json:"name" elastic_mapping:"name:{type:keyword,copy_to:combined_fulltext,fields:{text: {type: text}, pinyin: {type: text, analyzer: pinyin_analyzer}}}"`
    Description string `json:"description,omitempty" elastic_mapping:"description:{type:text,copy_to:combined_fulltext}"`
    Icon        string `json:"icon,omitempty" elastic_mapping:"icon:{enabled:false}"`
    Category    string `json:"category,omitempty" elastic_mapping:"category:{type:keyword}"`
    Tags        []string `json:"tags,omitempty" elastic_mapping:"tags:{type:keyword}"`
    Connector   ConnectorConfig `json:"connector,omitempty" elastic_mapping:"connector:{type:object}"`
    Enabled     bool   `json:"enabled" elastic_mapping:"enabled:{type:keyword}"`
}

Advanced Object Definition with Elasticsearch Mapping #

type Document struct {
    orm.ORMObjectBase        // Embedding ORM base for persistence-related fields

    Title       string    `json:"title" elastic_mapping:"title: { type: text, analyzer: standard }"`
    Content     string    `json:"content" elastic_mapping:"content: { type: text, analyzer: standard }"`
    Tags        []string  `json:"tags" elastic_mapping:"tags: { type: keyword }"`
    Status      string    `json:"status" elastic_mapping:"status: { type: keyword }"`

    Source      DataSourceReference `json:"source"`
}

Object Registration #

Objects must be registered with the ORM before use. Registration typically happens during application initialization. Note: Schema initialization is handled automatically - you only need to register your objects.

Simple Registration #

// Register object with custom index name
orm.MustRegisterSchemaWithIndexName(&User{}, "users")

// Register with default naming (struct name lowercase + 's')
orm.MustRegisterSchemaWithIndexName(&Document{}, "documents")

// Registration is complete - no additional initialization needed!

Registration with Context #

// For advanced scenarios with sharing/multitenancy
ctx := orm.NewContext()
orm.WithModel(ctx, &User{})
orm.WithModel(ctx, &Document{})

Elasticsearch Mapping with elastic_mapping #

The elastic_mapping tag allows you to define Elasticsearch field mappings directly in your Go struct. This provides fine-grained control over how your data is indexed and queried.

Common Mapping Parameters #

ParameterDescriptionExample
typeField data typetype:text, type:keyword, type:date
analyzerText analyzer for indexing and searchinganalyzer:standard, analyzer:ik_max_word
indexWhether field should be indexedindex:false, index:true
enabledEnable/disable field processingenabled:false, enabled:true
storeStore field values separatelystore:true
copy_toCopy field value to another fieldcopy_to:combined_fulltext
fieldsDefine multi-fields for different analysisfields:{keyword: {type: keyword}}
formatDate format for date fieldsformat:yyyy-MM-dd HH:mm:ss

Advanced Mapping Options #

Multi-field Mapping #

type Product struct {
    orm.ORMObjectBase

    Name string `json:"name" elastic_mapping:"name:{type:text,analyzer:standard,fields:{keyword:{type:keyword},raw:{type:keyword,index:false}}}"`
}

Object Mapping #

type User struct {
    orm.ORMObjectBase

    Address Address `json:"address" elastic_mapping:"address:{type:object,properties:{city:{type:keyword},country:{type:keyword}}}"`
}

// OR dynamic object mapping
type Config struct {
    orm.ORMObjectBase

    Settings map[string]interface{} `json:"settings" elastic_mapping:"settings:{type:object}"`
}

Store-only Mapping #

type Attachment struct {
    orm.ORMObjectBase

    FileName string `json:"filename"`
    FileData string `json:"file_data" elastic_mapping:"file_data:{enabled:false}"` // Not indexed, just stored
}

Text Analysis with Multiple Analyzers #

type Content struct {
    orm.ORMObjectBase

    Title string `json:"title" elastic_mapping:"title:{type:text,analyzer:standard,fields:{pinyin:{type:text,analyzer:pinyin_analyzer},keyword:{type:keyword}}}"`
}

Real-World DataSource Example #

From the actual codebase showing advanced mapping patterns:

type CombinedFullText struct {
    orm.ORMObjectBase
    CombinedFullText  string `json:"-" elastic_mapping:"combined_fulltext:{type:text,index_prefixes:{},index_phrases:true,analyzer:combined_text_analyzer}"`

    Metadata map[string]interface{} `json:"metadata,omitempty" elastic_mapping:"metadata:{type:object}"` // Searchable metadata
    Payload  map[string]interface{} `json:"payload,omitempty" elastic_mapping:"payload:{enabled:false}"` // Store-only metadata
}

type DataSource struct {
    CombinedFullText  // Inherits all base fields

    Type        string `json:"type,omitempty" elastic_mapping:"type:{type:keyword,copy_to:combined_fulltext}"`
    Name        string `json:"name" elastic_mapping:"name:{type:keyword,copy_to:combined_fulltext,fields:{text:{type:text},pinyin:{type:text,analyzer:pinyin_analyzer}}}"`
    Description string `json:"description,omitempty" elastic_mapping:"description:{type:text,copy_to:combined_fulltext}"`
    Icon        string `json:"icon,omitempty" elastic_mapping:"icon:{enabled:false}"`
    Category    string `json:"category,omitempty" elastic_mapping:"category:{type:keyword}"`
    Tags        []string `json:"tags,omitempty" elastic_mapping:"tags:{type:keyword}"`
    Connector   ConnectorConfig `json:"connector,omitempty" elastic_mapping:"connector:{type:object}"`
    Enabled     bool `json:"enabled" elastic_mapping:"enabled:{type:keyword}"`
}

CRUD Operations #

Create (Create) #

func createUser() {
    // Create context
    ctx := orm.NewContext()
    ctx.Refresh = orm.WaitForRefresh // Wait for index refresh

    // Create new user - ID and timestamps handled automatically by ORMObjectBase
    user := &User{
        Name:    "John Doe",
        Email:   "john@example.com",
        Age:     25,
    }

    // Insert into database
    err := orm.Create(ctx, user)
    if err != nil {
        log.Error("Failed to create user:", err)
        return
    }

    fmt.Printf("User created with ID: %s\n", user.GetID())
}

Read (Get) #

func getUser() {
    ctx := orm.NewContext()

    user := &User{}
    user.ID = "user-id-123"

    // Get user by ID
    exists, err := orm.GetV2(ctx, user)
    if err != nil {
        log.Error("Failed to get user:", err)
        return
    }

    if !exists {
        fmt.Println("User not found")
        return
    }

    fmt.Printf("Found user: %s, Email: %s\n", user.Name, user.Email)
}

func getUserWithSystemFields() {
    ctx := orm.NewContext()

    user := &User{}
    user.ID = "user-id-123"

    // Get user including system fields
    exists, err := orm.GetWithSystemFields(ctx, user)
    if err != nil {
        log.Error("Failed to get user:", err)
        return
    }

    // Access system fields
    if exists && user.System != nil {
        ownerID := user.GetOwnerID()
        fmt.Printf("User owner ID: %s\n", ownerID)
    }
}

Update (Update/Upsert) #

func updateUser() {
    ctx := orm.NewContext()
    ctx.Refresh = orm.WaitForRefresh

    // Get existing user - timestamps handled automatically
    user := &User{}
    user.SetID("user-id-123") // or use GetID() if you have the object

    exists, err := orm.GetV2(ctx, user)
    if err != nil || !exists {
        log.Error("User not found")
        return
    }

    // Update fields - Updated timestamp handled automatically
    user.Name = "John Smith"

    // Update in database
    err = orm.Update(ctx, user)
    if err != nil {
        log.Error("Failed to update user:", err)
        return
    }

    fmt.Println("User updated successfully")
}

func updatePartialFields() {
    ctx := orm.NewContext()
    ctx.Refresh = orm.WaitForRefresh

    // Update only specific fields
    updates := util.MapStr{
        "name":  "Johnny Doe",
        "email": "johnny@example.com",
    }

    user := &User{}
    user.ID = "user-id-123"

    err := orm.UpdatePartialFields(ctx, user, updates)
    if err != nil {
        log.Error("Failed to update user:", err)
        return
    }

    fmt.Println("User partially updated successfully")
}

func upsertUser() {
    ctx := orm.NewContext()
    ctx.Refresh = orm.WaitForRefresh

    // Create or update user - timestamps handled automatically
    user := &User{
        Name:    "John Updated",
        Email:   "john.updated@example.com",
    }

    // Use existing ID if updating, or SetID will generate one if needed
    user.SetID("user-id-123")

    err := orm.Upsert(ctx, user)
    if err != nil {
        log.Error("Failed to upsert user:", err)
        return
    }

    fmt.Println("User upserted successfully")
}

Delete (Delete) #

func deleteUser() {
    ctx := orm.NewContext()
    ctx.Refresh = orm.WaitForRefresh

    user := &User{}
    user.SetID("user-id-123")

    // Delete user
    err := orm.Delete(ctx, user)
    if err != nil {
        log.Error("Failed to delete user:", err)
        return
    }

    fmt.Println("User deleted successfully")
}

Advanced Operations #

Search with Query Builder #

func searchUsers() {
    ctx := orm.NewContext()

    // Create query builder
    builder := orm.NewQueryBuilder()

    // Add filters
    builder.Filter(orm.TermQuery("age", 25))
    builder.Filter(orm.RangeQuery("created", util.MapStr{
        "gte": "2023-01-01",
        "lte": "2023-12-31",
    }))

    // Add sorting
    builder.SortBy(orm.Sort{Field: "created", SortType: orm.DESC})

    // Execute search
    var users []User
    err, result := elastic.SearchV2WithResultItemMapper(ctx, &users, builder, nil)
    if err != nil {
        log.Error("Search failed:", err)
        return
    }

    fmt.Printf("Found %d users\n", len(users))
    for _, user := range users {
        fmt.Printf("User: %s (%s)\n", user.Name, user.Email)
    }
}

Complex Search with Text Queries #

func searchDocuments() {
    ctx := orm.NewContext()

    builder := orm.NewQueryBuilderFromRequest(req, "title", "content")

    // Enable body bytes for receiving additional Raw QueryDSL
    builder.EnableBodyBytes()

    // Add date range filter
    builder.Filter(orm.RangeQuery("created", util.MapStr{
        "gte": "2024-01-01",
    }))

    // Add pagination
    builder.Size(20).From(0)

    // Add aggregations
    ctx.Set(orm.AggsTerms, "tags")

    var docs []Document
    err, result := elastic.SearchV2WithResultItemMapper(ctx, &docs, builder, nil)
    if err != nil {
        log.Error("Search failed:", err)
        return
    }

    // Process results
    fmt.Printf("Found %d documents\n", len(docs))
}

Delete by Query #

func deleteOldUsers() {
    ctx := orm.NewContext()

    // Create delete query
    builder := orm.NewQueryBuilder()
    builder.Filter(orm.RangeQuery("created", util.MapStr{
        "lt": "2022-01-01", // Delete users created before 2022
    }))

    // Execute delete by query
    result, err := orm.DeleteByQuery(ctx, builder)
    if err != nil {
        log.Error("Delete by query failed:", err)
        return
    }

    fmt.Printf("Deleted %d old users\n", result.Deleted)
}

Context Options #

The ORM provides various context options for controlling behavior:

ctx := orm.NewContext()

// Wait for index refresh
ctx.Refresh = orm.WaitForRefresh

// Enable sharing for multi-tenant systems
ctx.Set(orm.SharingEnabled, true)
ctx.Set(orm.SharingResourceType, "users")
ctx.Set(orm.SharingCategoryCheckingChildrenEnabled, true)

// Keep system fields
ctx.Set(orm.KeepSystemFields, true)

// Model binding for type-safe operations
orm.WithModel(ctx, &User{})

// Set custom timeout
ctx.SetTimeout(30 * time.Second)

Real-World Example: DataSource Module #

Here’s how the ORM is used in the actual codebase:

func (h *APIHandler) createDatasource(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
    var obj = &core.DataSource{}
    h.MustDecodeJSON(req, obj)

    // Check referenced connector
    if obj.Connector.ConnectorID == "" {
        panic("invalid connector")
    }

    ctx := orm.NewContextWithParent(req.Context())

    // Validate related object
    connector := core.Connector{}
    connector.ID = obj.Connector.ConnectorID
    exists, err := orm.GetV2(ctx, &connector)
    if !exists || err != nil {
        panic("invalid connector")
    }

    // Set refresh option and create
    ctx.Refresh = orm.WaitForRefresh
    err = orm.Create(ctx, obj)
    if err != nil {
        h.WriteError(w, err.Error(), http.StatusInternalServerError)
        return
    }

    h.WriteJSON(w, util.MapStr{
        "_id":    obj.ID,
        "result": "created",
    }, 200)
}
Edit Edit this page