Files
elowdb-go/CUSTOM_JSON.md

10 KiB
Raw Blame History

Настраиваемые функции сериализации JSON в LineDb

Обзор

LineDb поддерживает настраиваемые функции сериализации и десериализации JSON. По умолчанию используется библиотека go-json, но вы можете указать любые функции сериализации, соответствующие определенным сигнатурам.

Функции по умолчанию

// Функции сериализации по умолчанию (используют go-json)
func defaultJSONMarshal(v any) ([]byte, error) {
    return json.Marshal(v)
}

func defaultJSONUnmarshal(data []byte, v any) error {
    return json.Unmarshal(data, v)
}

Настройка в опциях коллекции

Вы можете указать пользовательские функции сериализации в JSONLFileOptions:

type JSONLFileOptions struct {
    CollectionName          string   `json:"collectionName,omitempty"`
    AllocSize               int      `json:"allocSize,omitempty"`
    IndexedFields           []string `json:"indexedFields,omitempty"`
    EncryptKeyForLineDb     string   `json:"encryptKeyForLineDb,omitempty"`
    SkipInvalidLines        bool     `json:"skipInvalidLines,omitempty"`
    DecryptKey              string   `json:"decryptKey,omitempty"`
    ConvertStringIdToNumber bool     `json:"convertStringIdToNumber,omitempty"`
    // Функции сериализации и десериализации JSON
    JSONMarshal   func(any) ([]byte, error) `json:"-"`
    JSONUnmarshal func([]byte, any) error   `json:"-"`
}

Примеры использования

1. Использование стандартного encoding/json

import "encoding/json"

// Создаем функции сериализации с использованием стандартного encoding/json
standardJSONMarshal := func(v any) ([]byte, error) {
    return json.Marshal(v)
}

standardJSONUnmarshal := func(data []byte, v any) error {
    return json.Unmarshal(data, v)
}

// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
    Collections: []linedb.JSONLFileOptions{
        {
            CollectionName: "users",
            JSONMarshal:    standardJSONMarshal,
            JSONUnmarshal:  standardJSONUnmarshal,
        },
    },
}

2. Кастомные функции с метаданными

import "encoding/json"
import "time"

// Создаем кастомные функции сериализации с дополнительной логикой
customJSONMarshal := func(v any) ([]byte, error) {
    // Добавляем метаданные к сериализации
    data := map[string]any{
        "data":      v,
        "timestamp": time.Now().Unix(),
        "version":   "1.0",
    }
    return json.Marshal(data)
}

customJSONUnmarshal := func(data []byte, v any) error {
    // Извлекаем данные из кастомного формата
    var wrapper map[string]any
    if err := json.Unmarshal(data, &wrapper); err != nil {
        return err
    }

    // Извлекаем основную часть данных
    if dataField, exists := wrapper["data"]; exists {
        // Сериализуем обратно в JSON и десериализуем в целевой тип
        dataBytes, err := json.Marshal(dataField)
        if err != nil {
            return err
        }
        return json.Unmarshal(dataBytes, v)
    }

    // Если нет поля data, пытаемся десериализовать как обычный JSON
    return json.Unmarshal(data, v)
}

// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
    Collections: []linedb.JSONLFileOptions{
        {
            CollectionName: "users",
            JSONMarshal:    customJSONMarshal,
            JSONUnmarshal:  customJSONUnmarshal,
        },
    },
}

3. Функции с шифрованием

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
)

var encryptionKey = []byte("your-32-byte-encryption-key-here")

func encryptedJSONMarshal(v any) ([]byte, error) {
    // Сначала сериализуем в JSON
    jsonData, err := json.Marshal(v)
    if err != nil {
        return nil, err
    }

    // Создаем AES cipher
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return nil, err
    }

    // Создаем GCM
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    // Создаем nonce
    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }

    // Шифруем данные
    ciphertext := gcm.Seal(nonce, nonce, jsonData, nil)

    // Кодируем в base64
    return []byte(base64.StdEncoding.EncodeToString(ciphertext)), nil
}

func encryptedJSONUnmarshal(data []byte, v any) error {
    // Декодируем из base64
    ciphertext, err := base64.StdEncoding.DecodeString(string(data))
    if err != nil {
        return err
    }

    // Создаем AES cipher
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return err
    }

    // Создаем GCM
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return err
    }

    // Извлекаем nonce
    nonceSize := gcm.NonceSize()
    if len(ciphertext) < nonceSize {
        return fmt.Errorf("ciphertext too short")
    }

    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

    // Расшифровываем данные
    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return err
    }

    // Десериализуем JSON
    return json.Unmarshal(plaintext, v)
}

// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
    Collections: []linedb.JSONLFileOptions{
        {
            CollectionName: "secure_users",
            JSONMarshal:    encryptedJSONMarshal,
            JSONUnmarshal:  encryptedJSONUnmarshal,
        },
    },
}

4. ID всегда первое поле

import (
    "encoding/json"
    "fmt"
    "sort"
    "strings"
)

func idFirstJSONMarshal(v any) ([]byte, error) {
    if data, ok := v.(map[string]any); ok {
        var parts []string
        
        // Сначала добавляем id если он есть
        if id, exists := data["id"]; exists {
            idBytes, err := json.Marshal(id)
            if err != nil {
                return nil, err
            }
            parts = append(parts, fmt.Sprintf(`"id":%s`, string(idBytes)))
        }
        
        // Затем добавляем все остальные поля в алфавитном порядке
        var keys []string
        for key := range data {
            if key != "id" {
                keys = append(keys, key)
            }
        }
        sort.Strings(keys)
        
        for _, key := range keys {
            valueBytes, err := json.Marshal(data[key])
            if err != nil {
                return nil, err
            }
            parts = append(parts, fmt.Sprintf(`"%s":%s`, key, string(valueBytes)))
        }
        
        jsonStr := "{" + strings.Join(parts, ",") + "}"
        return []byte(jsonStr), nil
    }
    
    // Если это не map, используем стандартную сериализацию
    return json.Marshal(v)
}

func idFirstJSONUnmarshal(data []byte, v any) error {
    return json.Unmarshal(data, v)
}

// Настройка в опциях инициализации
initOptions := &linedb.LineDbInitOptions{
    Collections: []linedb.JSONLFileOptions{
        {
            CollectionName: "users",
            JSONMarshal:    idFirstJSONMarshal,
            JSONUnmarshal:  idFirstJSONUnmarshal,
        },
    },
}

Требования к функциям

JSONMarshal

  • Сигнатура: func(any) ([]byte, error)
  • Назначение: Сериализует данные в JSON
  • Возвращает: Байты JSON и ошибку (если есть)

JSONUnmarshal

  • Сигнатура: func([]byte, any) error
  • Назначение: Десериализует данные из JSON
  • Возвращает: Ошибку (если есть)

Совместимость

При использовании кастомных функций сериализации важно обеспечить совместимость:

  1. Обратная совместимость: Убедитесь, что новые функции могут читать данные, записанные старыми функциями
  2. Версионирование: Рассмотрите добавление версии в метаданные для будущих изменений
  3. Тестирование: Всегда тестируйте функции сериализации с реальными данными

Примеры файлов

Стандартный JSON (go-json по умолчанию)

{"id":1,"username":"user1","email":"user1@example.com"}

Стандартный JSON (encoding/json)

{"id":2,"username":"user2","email":"user2@example.com"}

Кастомный JSON с метаданными

{
  "data": {"id":3,"username":"user3","email":"user3@example.com"},
  "timestamp": 1754969076,
  "version": "1.0"
}

JSON с ID первым полем

{"id":1,"createdAt":1754969435,"email":"user@example.com","isActive":true,"role":"user","username":"user1"}

Запуск примера

cd examples/custom-json
go run main.go

Этот пример демонстрирует:

  • Использование go-json (по умолчанию)
  • Использование стандартного encoding/json
  • Кастомные функции сериализации с метаданными
  • Функции сериализации с ID первым полем

Тестирование

cd examples/custom-json
go test -v

Тесты проверяют:

  • Корректность сериализации/десериализации
  • Работу с JSONLFile
  • Обработку ошибок