重构creatingUniqueIndex和createGinIndexes

This commit is contained in:
2025-11-20 17:46:01 +08:00
parent da934a9bbb
commit 1313140e45
2 changed files with 156 additions and 122 deletions

View File

@@ -6,6 +6,7 @@ package database
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
@@ -265,144 +266,169 @@ func (ps *PostgresStorage) creatingIndex(ctx context.Context) error {
func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error { func (ps *PostgresStorage) creatingUniqueIndex(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingUniqueIndex") storageCtx, logger := logs.Trace(ctx, ps.ctx, "creatingUniqueIndex")
// 为 raw_material_nutrients 表创建部分唯一索引,以兼容软删除 // uniqueIndexDefinition 结构体定义了唯一索引的详细信息
logger.Debug("正在为 raw_material_nutrients 表创建部分唯一索引") type uniqueIndexDefinition struct {
partialIndexSQL := "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_material_nutrients_unique_when_not_deleted ON raw_material_nutrients (raw_material_id, nutrient_id) WHERE deleted_at IS NULL;" tableName string // 索引所属的表名
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { columns []string // 构成唯一索引的列名
logger.Errorw("为 raw_material_nutrients 创建部分唯一索引失败", "error", err) indexName string // 唯一索引的名称
return fmt.Errorf("为 raw_material_nutrients 创建部分唯一索引失败: %w", err) whereClause string // 可选的 WHERE 子句,用于创建部分索引
description string // 索引的描述,用于日志记录
} }
logger.Debug("成功为 raw_material_nutrients 创建部分唯一索引 (或已存在)")
// 为 pig_breeds 表创建部分唯一索引,以兼容软删除 (name 唯一) // 定义所有需要创建的唯一索引
logger.Debug("正在为 pig_breeds 表创建部分唯一索引") uniqueIndexesToCreate := []uniqueIndexDefinition{
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_breeds_unique_name_when_not_deleted ON pig_breeds (name) WHERE deleted_at IS NULL;" {
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { tableName: models.RawMaterialNutrient{}.TableName(),
logger.Errorw("为 pig_breeds 创建部分唯一索引失败", "error", err) columns: []string{"raw_material_id", "nutrient_id"},
return fmt.Errorf("为 pig_breeds 创建部分唯一索引失败: %w", err) indexName: "idx_raw_material_nutrients_unique_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "确保同一原料中的每种营养成分不重复",
},
{
tableName: models.PigBreed{}.TableName(),
columns: []string{"name"},
indexName: "idx_pig_breeds_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_breeds 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.PigAgeStage{}.TableName(),
columns: []string{"name"},
indexName: "idx_pig_age_stages_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_age_stages 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.PigType{}.TableName(),
columns: []string{"breed_id", "age_stage_id"},
indexName: "idx_pig_types_unique_breed_age_stage_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_types 表的部分唯一索引 (breed_id, age_stage_id 组合唯一)",
},
{
tableName: models.PigNutrientRequirement{}.TableName(),
columns: []string{"pig_type_id", "nutrient_id"},
indexName: "idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_nutrient_requirements 表的部分唯一索引 (pig_type_id, nutrient_id 组合唯一)",
},
{
tableName: models.User{}.TableName(),
columns: []string{"username"},
indexName: "idx_users_unique_username_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "users 表的部分唯一索引 (username 唯一)",
},
{
tableName: models.AreaController{}.TableName(),
columns: []string{"name"},
indexName: "idx_area_controllers_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "area_controllers 表的部分唯一索引 (Name 唯一)",
},
{
tableName: models.AreaController{}.TableName(),
columns: []string{"network_id"},
indexName: "idx_area_controllers_unique_network_id_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "area_controllers 表的部分唯一索引 (NetworkID 唯一)",
},
{
tableName: models.DeviceTemplate{}.TableName(),
columns: []string{"name"},
indexName: "idx_device_templates_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "device_templates 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.PigBatch{}.TableName(),
columns: []string{"batch_number"},
indexName: "idx_pig_batches_unique_batch_number_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_batches 表的部分唯一索引 (batch_number 唯一)",
},
{
tableName: models.PigHouse{}.TableName(),
columns: []string{"name"},
indexName: "idx_pig_houses_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "pig_houses 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.RawMaterial{}.TableName(),
columns: []string{"name"},
indexName: "idx_raw_materials_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "raw_materials 表的部分唯一索引 (name 唯一)",
},
{
tableName: models.Nutrient{}.TableName(),
columns: []string{"name"},
indexName: "idx_nutrients_unique_name_when_not_deleted",
whereClause: "WHERE deleted_at IS NULL",
description: "nutrients 表的部分唯一索引 (name 唯一)",
},
} }
logger.Debug("成功为 pig_breeds 创建部分唯一索引 (或已存在)")
// 为 pig_age_stages 表创建部分唯一索引,以兼容软删除 (name 唯一) for _, indexDef := range uniqueIndexesToCreate {
logger.Debug("正在为 pig_age_stages 表创建部分唯一索引") logger.Debugw("正在为表创建部分唯一索引", "表名", indexDef.tableName, "索引名", indexDef.indexName, "描述", indexDef.description)
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_age_stages_unique_name_when_not_deleted ON pig_age_stages (name) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 pig_age_stages 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 pig_age_stages 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 pig_age_stages 创建部分唯一索引 (或已存在)")
// 为 pig_types 表创建部分唯一索引,以兼容软删除 (breed_id, age_stage_id 组合唯一) // 拼接列名字符串
logger.Debug("正在为 pig_types 表创建部分唯一索引") columnsStr := strings.Join(indexDef.columns, ", ")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_types_unique_breed_age_stage_when_not_deleted ON pig_types (breed_id, age_stage_id) WHERE deleted_at IS NULL;" // 构建 SQL 语句
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { sql := fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s) %s;",
logger.Errorw("为 pig_types 创建部分唯一索引失败", "error", err) indexDef.indexName, indexDef.tableName, columnsStr, indexDef.whereClause)
return fmt.Errorf("为 pig_types 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 pig_types 创建部分唯一索引 (或已存在)")
// 为 pig_nutrient_requirements 表创建部分唯一索引,以兼容软删除 (pig_type_id, nutrient_id 组合唯一) if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil {
logger.Debug("正在为 pig_nutrient_requirements 表创建部分唯一索引") logger.Errorw("创建部分唯一索引失败", "表名", indexDef.tableName, "索引名", indexDef.indexName, "错误", err)
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_nutrient_requirements_unique_type_nutrient_when_not_deleted ON pig_nutrient_requirements (pig_type_id, nutrient_id) WHERE deleted_at IS NULL;" return fmt.Errorf("为 %s 表创建部分唯一索引 %s 失败: %w", indexDef.tableName, indexDef.indexName, err)
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil { }
logger.Errorw("为 pig_nutrient_requirements 创建部分唯一索引失败", "error", err) logger.Debugw("成功为表创建部分唯一索引 (或已存在)", "表名", indexDef.tableName, "索引名", indexDef.indexName)
return fmt.Errorf("为 pig_nutrient_requirements 创建部分唯一索引失败: %w", err)
} }
logger.Debug("成功为 pig_nutrient_requirements 创建部分唯一索引 (或已存在)")
// 为 users 表创建部分唯一索引
logger.Debug("正在为 users 表创建部分唯一索引")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_unique_username_when_not_deleted ON users (username) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 users 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 users 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 users 创建部分唯一索引 (或已存在)")
// 为 area_controllers 表创建部分唯一索引 (Name)
logger.Debug("正在为 area_controllers 表创建部分唯一索引 (Name)")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_name_when_not_deleted ON area_controllers (name) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 area_controllers 创建部分唯一索引 (Name) 失败", "error", err)
return fmt.Errorf("为 area_controllers 创建部分唯一索引 (Name) 失败: %w", err)
}
logger.Debug("成功为 area_controllers 创建部分唯一索引 (Name) (或已存在)")
// 为 area_controllers 表创建部分唯一索引 (NetworkID)
logger.Debug("正在为 area_controllers 表创建部分唯一索引 (NetworkID)")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_area_controllers_unique_network_id_when_not_deleted ON area_controllers (network_id) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 area_controllers 创建部分唯一索引 (NetworkID) 失败", "error", err)
return fmt.Errorf("为 area_controllers 创建部分唯一索引 (NetworkID) 失败: %w", err)
}
logger.Debug("成功为 area_controllers 创建部分唯一索引 (NetworkID) (或已存在)")
// 为 device_templates 表创建部分唯一索引
logger.Debug("正在为 device_templates 表创建部分唯一索引")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_device_templates_unique_name_when_not_deleted ON device_templates (name) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 device_templates 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 device_templates 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 device_templates 创建部分唯一索引 (或已存在)")
// 为 pig_batches 表创建部分唯一索引
logger.Debug("正在为 pig_batches 表创建部分唯一索引")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_batches_unique_batch_number_when_not_deleted ON pig_batches (batch_number) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 pig_batches 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 pig_batches 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 pig_batches 创建部分唯一索引 (或已存在)")
// 为 pig_houses 表创建部分唯一索引
logger.Debug("正在为 pig_houses 表创建部分唯一索引")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_pig_houses_unique_name_when_not_deleted ON pig_houses (name) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 pig_houses 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 pig_houses 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 pig_houses 创建部分唯一索引 (或已存在)")
// 为 raw_materials 表创建部分唯一索引
logger.Debug("正在为 raw_materials 表创建部分唯一索引")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_materials_unique_name_when_not_deleted ON raw_materials (name) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 raw_materials 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 raw_materials 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 raw_materials 创建部分唯一索引 (或已存在)")
// 为 nutrients 表创建部分唯一索引
logger.Debug("正在为 nutrients 表创建部分唯一索引")
partialIndexSQL = "CREATE UNIQUE INDEX IF NOT EXISTS idx_nutrients_unique_name_when_not_deleted ON nutrients (name) WHERE deleted_at IS NULL;"
if err := ps.db.WithContext(storageCtx).Exec(partialIndexSQL).Error; err != nil {
logger.Errorw("为 nutrients 创建部分唯一索引失败", "error", err)
return fmt.Errorf("为 nutrients 创建部分唯一索引失败: %w", err)
}
logger.Debug("成功为 nutrients 创建部分唯一索引 (或已存在)")
return nil return nil
} }
func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error { func (ps *PostgresStorage) createGinIndexes(ctx context.Context) error {
storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes") storageCtx, logger := logs.Trace(ctx, ps.ctx, "createGinIndexes")
// 为 sensor_data 表的 data 字段创建 GIN 索引 // ginIndexDefinition 结构体定义了 GIN 索引的详细信息
logger.Debug("正在为 sensor_data 表的 data 字段创建 GIN 索引") type ginIndexDefinition struct {
ginSensorDataIndexSQL := "CREATE INDEX IF NOT EXISTS idx_sensor_data_data_gin ON sensor_data USING GIN (data);" tableName string // 索引所属的表名
if err := ps.db.WithContext(storageCtx).Exec(ginSensorDataIndexSQL).Error; err != nil { columnName string // 需要创建 GIN 索引的列名
logger.Errorw("为 sensor_data 的 data 字段创建 GIN 索引失败", "error", err) indexName string // GIN 索引的名称
return fmt.Errorf("为 sensor_data 的 data 字段创建 GIN 索引失败: %w", err) description string // 索引的描述,用于日志记录
} }
logger.Debug("成功为 sensor_data 的 data 字段创建 GIN 索引 (或已存在)")
// 为 tasks.parameters 创建 GIN 索引 // 定义所有需要创建 GIN 索引
logger.Debug("正在为 tasks 表的 parameters 字段创建 GIN 索引") ginIndexesToCreate := []ginIndexDefinition{
taskGinIndexSQL := "CREATE INDEX IF NOT EXISTS idx_tasks_parameters_gin ON tasks USING GIN (parameters);" {
if err := ps.db.WithContext(storageCtx).Exec(taskGinIndexSQL).Error; err != nil { tableName: "sensor_data",
logger.Errorw("为 tasks 的 parameters 字段创建 GIN 索引失败", "error", err) columnName: "data",
return fmt.Errorf("为 tasks 的 parameters 字段创建 GIN 索引失败: %w", err) indexName: "idx_sensor_data_data_gin",
description: "为 sensor_data 表的 data 字段创建 GIN 索引",
},
{
tableName: "tasks",
columnName: "parameters",
indexName: "idx_tasks_parameters_gin",
description: "为 tasks 表的 parameters 字段创建 GIN 索引",
},
} }
logger.Debug("成功为 tasks 的 parameters 字段创建 GIN 索引 (或已存在)")
for _, indexDef := range ginIndexesToCreate {
logger.Debugw("正在创建 GIN 索引", "表名", indexDef.tableName, "列名", indexDef.columnName, "描述", indexDef.description)
// 构建 SQL 语句
sql := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s USING GIN (%s);",
indexDef.indexName, indexDef.tableName, indexDef.columnName)
if err := ps.db.WithContext(storageCtx).Exec(sql).Error; err != nil {
logger.Errorw("创建 GIN 索引失败", "表名", indexDef.tableName, "索引名", indexDef.indexName, "错误", err)
return fmt.Errorf("为 %s 表的 %s 字段创建 GIN 索引 %s 失败: %w", indexDef.tableName, indexDef.columnName, indexDef.indexName, err)
}
logger.Debugw("成功创建 GIN 索引 (或已存在)", "表名", indexDef.tableName, "索引名", indexDef.indexName)
}
return nil return nil
} }

View File

@@ -12,6 +12,10 @@ type PigHouse struct {
Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏 Pens []Pen `gorm:"foreignKey:HouseID"` // 一个猪舍包含多个猪栏
} }
func (ph PigHouse) TableName() string {
return "pig_houses"
}
// PenStatus 定义了猪栏的当前状态 // PenStatus 定义了猪栏的当前状态
type PenStatus string type PenStatus string
@@ -33,3 +37,7 @@ type Pen struct {
Capacity int `gorm:"not null;comment:设计容量 (头)"` Capacity int `gorm:"not null;comment:设计容量 (头)"`
Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"` Status PenStatus `gorm:"not null;index;comment:猪栏当前状态"`
} }
func (p Pen) TableName() string {
return "pens"
}