You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang中实现可动态切换的多数据库仓库方案问询

问题说明

当前运行的Go API通过仓库层从MySQL数据库获取数据,需要实现相同的仓库逻辑以从MongoDB获取数据,同时要求能根据API配置动态切换数据库(MySQL/MongoDB)。现有代码仅支持单一数据库模式,尝试过三种方案后,希望让「不同仓库实现」的方案可行,避免大量修改现有调用代码。

现有代码

// My vehicle repository file

// Repository handling Vehicle
type VehicleRepository struct {
    conn  *sqlx.DB
}

func (r *VehicleRepository) GetVehicles(ctx context.Context, brand string) ([]*vehicle.Vehicle, error) {
    var vehicles []*vehicle.Vehicle
    param := map[string]interface{}{"brand": brand}

    query := "SELECT * FROM vehicles WHERE brand = :brand"

    rows, err := r.conn.NamedQueryContext(ctx, query, param)
    if err != nil {
        return nil, errors.Wrap(err, "query execution")
    }

    return vehicles, nil
}

// Repository集合定义(实际包含30+仓库)
type Repositories struct {
    Vehicle    *VehicleRepository
}

func CreateRepositories(conn c.Connections) *Repositories {
    return &Repositories{
        Vehicle:      &VehicleRepository{conn, l},
    }
}

已尝试的方案

  • 方案1:拆分仓库实现,将原vehicle.go改为vehicle.mysql.go并创建vehicle.mongodb.go,分别实现VehicleMySQLRepository和VehicleMongoDBRepository,但无法在Repositories结构体中统一类型,且修改50+调用文件成本过高。
  • 方案2:条件编译,通过go build -tags mongo编译对应版本,但无法动态切换,不符合前端用户可选择数据库的需求。
  • 方案3:同一仓库兼容双数据库,在GetVehicles等方法中加入条件判断处理两种数据库逻辑,虽可行但代码臃肿。

解决方案:基于接口统一仓库类型

核心思路是定义仓库接口,让MySQL和MongoDB的仓库实现都满足该接口,然后在Repositories中使用接口类型而非具体结构体,这样调用代码无需修改,仅需在创建仓库时根据配置动态选择实例。

步骤1:定义仓库接口

在vehicle包或单独的仓库接口文件中定义接口:

// vehicle/repository.go
package vehicle

import "context"

// VehicleRepository 定义车辆仓库的核心方法
type VehicleRepository interface {
    GetVehicles(ctx context.Context, brand string) ([]*Vehicle, error)
    // 其他需要的方法(如CreateVehicle、UpdateVehicle等)
}

步骤2:实现MySQL和MongoDB仓库

分别实现接口,保持原有逻辑不变:

// vehicle/repository_mysql.go
package vehicle

import (
    "context"
    "errors"
    "github.com/jmoiron/sqlx"
)

type VehicleMySQLRepository struct {
    conn *sqlx.DB
}

// 编译期检查是否实现接口
var _ VehicleRepository = (*VehicleMySQLRepository)(nil)

func NewVehicleMySQLRepository(conn *sqlx.DB) *VehicleMySQLRepository {
    return &VehicleMySQLRepository{conn: conn}
}

func (r *VehicleMySQLRepository) GetVehicles(ctx context.Context, brand string) ([]*Vehicle, error) {
    var vehicles []*Vehicle
    param := map[string]interface{}{"brand": brand}
    query := "SELECT * FROM vehicles WHERE brand = :brand"

    rows, err := r.conn.NamedQueryContext(ctx, query, param)
    if err != nil {
        return nil, errors.Wrap(err, "mysql query execution")
    }
    defer rows.Close()

    // 补充原代码缺失的行扫描逻辑
    for rows.Next() {
        var v Vehicle
        if err := rows.StructScan(&v); err != nil {
            return nil, errors.Wrap(err, "scan vehicle row")
        }
        vehicles = append(vehicles, &v)
    }
    if err := rows.Err(); err != nil {
        return nil, errors.Wrap(err, "rows iteration error")
    }

    return vehicles, nil
}
// vehicle/repository_mongodb.go
package vehicle

import (
    "context"
    "errors"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
)

type VehicleMongoDBRepository struct {
    coll *mongo.Collection
}

// 编译期检查是否实现接口
var _ VehicleRepository = (*VehicleMongoDBRepository)(nil)

func NewVehicleMongoDBRepository(coll *mongo.Collection) *VehicleMongoDBRepository {
    return &VehicleMongoDBRepository{coll: coll}
}

func (r *VehicleMongoDBRepository) GetVehicles(ctx context.Context, brand string) ([]*Vehicle, error) {
    var vehicles []*Vehicle
    filter := bson.M{"brand": brand}

    cursor, err := r.coll.Find(ctx, filter)
    if err != nil {
        return nil, errors.Wrap(err, "mongodb find execution")
    }
    defer cursor.Close(ctx)

    if err := cursor.All(ctx, &vehicles); err != nil {
        return nil, errors.Wrap(err, "decode mongodb documents")
    }

    return vehicles, nil
}

步骤3:修改Repositories结构体和创建函数

将具体结构体替换为接口类型,并根据配置动态实例化对应仓库:

// repositories.go
type Repositories struct {
    Vehicle    vehicle.VehicleRepository // 使用接口类型
    // 其他仓库也替换为对应的接口
}

// 假设配置包含数据库类型标识
type Config struct {
    DatabaseType string // 可选值:"mysql" / "mongodb"
    // 其他配置项(如MySQL连接、MongoDB连接)
}

func CreateRepositories(conn c.Connections, cfg Config) *Repositories {
    repos := &Repositories{}

    switch cfg.DatabaseType {
    case "mysql":
        repos.Vehicle = vehicle.NewVehicleMySQLRepository(conn.MySQL)
    case "mongodb":
        repos.Vehicle = vehicle.NewVehicleMongoDBRepository(conn.MongoDB.VehiclesColl)
    default:
        panic("unsupported database type")
    }

    // 初始化其他仓库...
    return repos
}

步骤4:调用代码无需修改

原有调用repo.Vehicle.GetVehicles(ctx, brand)的代码完全不需要改动,因为接口方法签名与原结构体方法一致。

优势说明

  • 完全兼容现有调用代码,无需修改50+文件;
  • 符合开闭原则,新增数据库类型只需实现对应接口,不修改现有逻辑;
  • 支持动态切换,只需在创建仓库时根据配置选择实例;
  • 编译期检查接口实现,避免运行时错误。

内容的提问来源于stack exchange,提问作者Paul-Marie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 13:17:04