Как должна выглядеть правильная структура внедрения зависимостей при работе с несколькими базами данных в Go?
Добрый день! Я начинаю изучать разработку серверов на Go с использованием фреймворка Gin. Сейчас настраиваю вспомогательные структуры и зависимости. В первом проекте я использовал только одну базу данных, но теперь планирую масштабный проект, который будет включать PostgreSQL, MongoDB и Redis.
Проблема в следующем:
При работе с одной базой данных мой контейнер выглядел так:
type container struct {
config *config.Config
rep repository.Repository
logger logger.Logger
}
А репозиторий реализовывал методы для PostgreSQL:
type Repository interface {
Model(value interface{}) *gorm.DB
Select(query interface{}, args ...interface{}) *gorm.DB
Find(out interface{}, where ...interface{}) *gorm.DB
Exec(sql string, values ...interface{}) *gorm.DB
First(out interface{}, where ...interface{}) *gorm.DB
Raw(sql string, values ...interface{}) *gorm.DB
Create(value interface{}) *gorm.DB
Save(value interface{}) *gorm.DB
Updates(value interface{}) *gorm.DB
Delete(value interface{}) *gorm.DB
Where(query interface{}, args ...interface{}) *gorm.DB
Preload(column string, conditions ...interface{}) *gorm.DB
Scopes(funcs ...func(*gorm.DB) *gorm.DB) *gorm.DB
ScanRows(rows *sql.Rows, result interface{}) error
Transaction(fc func(tx Repository) error) (err error)
Close() error
DropTableIfExists(value interface{}) error
GetSQLDB() (*sql.DB, error)
Clauses(conds ...clause.Expression) *gorm.DB
AutoMigrate(value interface{}) error
}
type repository struct {
db *gorm.DB
}
type filmRepository struct {
*repository
}
Если сейчас все помещать в один репозиторий, получится перегруженный интерфейс. Я придумал такую реализацию:
type Repository interface {
Postgres() PostgresRepository
Mongo() MongoRepository
Redis() RedisRepository
}
То есть в основном интерфейсе хранить три вложенных интерфейса для каждой базы данных. Насколько это разумная структура? Я хочу сделать все правильно с самого начала, а не переделывать потом. Если есть альтернативные решения для таких задач, готов выслушать и принять к сведению.
Правильная структура внедрения зависимостей при работе с несколькими базами данных в Go должна обеспечивать разделение ответственности и удобное тестирование. Исходя из вашего подхода, я могу предложить несколько улучшенных решений.
Основной принцип: каждая база данных должна иметь свой собственный интерфейс репозитория и внедряться отдельно. Это следует принципу разделения интерфейсов (Interface Segregation Principle).
Содержание
- Основные подходы к структуре зависимостей
- Рекомендуемая архитектура с разделенными интерфейсами
- Альтернативные решения
- Практическая реализация контейнера зависимостей
- Рекомендации по организации кода
- Выводы и лучшие практики
Основные подходы к структуре зависимостей
1. Отдельные интерфейсы для каждой базы данных
Этот подход заключается в создании отдельных интерфейсов для каждой базы данных и их внедрении в контейнер зависимостей независимо:
// Интерфейсы для каждой базы данных
type PostgresRepository interface {
FindUser(id uint) (*User, error)
CreateUser(user *User) error
// другие специфичные для PostgreSQL методы
}
type MongoRepository interface {
FindDocument(collection string, filter interface{}) (interface{}, error)
InsertDocument(collection string, document interface{}) error
// другие специфичные для MongoDB методы
}
type RedisRepository interface {
Set(key string, value interface{}, expiration time.Duration) error
Get(key string) (string, error)
// другие специфичные для Redis методы
}
2. Ваш текущий подход с вложенными интерфейсами
Ваш подход с единым интерфейсом и методами Postgres(), Mongo(), Redis() - это компоновщик (facade), который может быть полезен, но имеет недостатки:
type Repository interface {
Postgres() PostgresRepository
Mongo() MongoRepository
Redis() RedisRepository
}
Преимущества:
- Упрощает передачу зависимостей в сервисы
- Обеспечивает единый точку доступа к репозиториям
Недостатки:
- Создает тесную связанность между компонентами
- Усложняет тестирование отдельных репозиториев
- Может привести к избыточным зависимостям
Рекомендуемая архитектура с разделенными интерфейсами
Наилучшей практикой является использование отдельных интерфейсов для каждой базы данных с внедрением их через контейнер зависимостей:
Структура контейнера зависимостей
type container struct {
config *config.Config
logger logger.Logger
postgresRepo PostgresRepository
mongoRepo MongoRepository
redisRepo RedisRepository
}
func NewContainer(cfg *config.Config, log logger.Logger) *container {
return &container{
config: cfg,
logger: log,
}
}
func (c *container) Postgres() PostgresRepository {
if c.postgresRepo == nil {
// Инициализация PostgreSQL
db, err := gorm.Open(postgres.Open(c.config.Database.URL), &gorm.Config{})
if err != nil {
c.logger.Fatal("Failed to connect to PostgreSQL", "error", err)
}
c.postgresRepo = &postgresRepository{db: db}
}
return c.postgresRepo
}
func (c *container) Mongo() MongoRepository {
if c.mongoRepo == nil {
// Инициализация MongoDB
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(c.config.MongoDB.URI))
if err != nil {
c.logger.Fatal("Failed to connect to MongoDB", "error", err)
}
c.mongoRepo = &mongoRepository{client: client}
}
return c.mongoRepo
}
func (c *container) Redis() RedisRepository {
if c.redisRepo == nil {
// Инициализация Redis
rdb := redis.NewClient(&redis.Options{
Addr: c.config.Redis.Address,
})
c.redisRepo = &redisRepository{client: rdb}
}
return c.redisRepo
}
Пример реализации репозиториев
PostgreSQL репозиторий:
type postgresRepository struct {
db *gorm.DB
}
func (r *postgresRepository) FindUser(id uint) (*User, error) {
var user User
result := r.db.First(&user, id)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
func (r *postgresRepository) CreateUser(user *User) error {
return r.db.Create(user).Error
}
// Реализация интерфейса PostgresRepository
MongoDB репозиторий:
type mongoRepository struct {
client *mongo.Client
}
func (r *mongoRepository) FindDocument(collection string, filter interface{}) (interface{}, error) {
coll := r.client.Database("app").Collection(collection)
var result bson.M
err := coll.FindOne(context.Background(), filter).Decode(&result)
if err != nil {
return nil, err
}
return result, nil
}
func (r *mongoRepository) InsertDocument(collection string, document interface{}) error {
coll := r.client.Database("app").Collection(collection)
_, err := coll.InsertOne(context.Background(), document)
return err
}
// Реализация интерфейса MongoRepository
Redis репозиторий:
type redisRepository struct {
client *redis.Client
}
func (r *redisRepository) Set(key string, value interface{}, expiration time.Duration) error {
return r.client.Set(context.Background(), key, value, expiration).Err()
}
func (r *redisRepository) Get(key string) (string, error) {
return r.client.Get(context.Background(), key).Result()
}
// Реализация интерфейса RedisRepository
Альтернативные решения
1. Использование фреймворков внедрения зависимостей
Для более сложных проектов можно использовать специализированные библиотеки:
import "go.uber.org/dig"
func BuildContainer() *dig.Container {
container := dig.New()
container.Provide(func() (PostgresRepository, error) {
// Инициализация PostgreSQL
db, err := gorm.Open(postgres.Open("..."), &gorm.Config{})
return &postgresRepository{db: db}, err
})
container.Provide(func() (MongoRepository, error) {
// Инициализация MongoDB
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI("..."))
return &mongoRepository{client: client}, err
})
container.Provide(func() (RedisRepository, error) {
// Инициализация Redis
rdb := redis.NewClient(&redis.Options{Addr: "..."})
return &redisRepository{client: rdb}, nil
})
return container
}
2. Сервисный слой с агрегацией
Создайте сервисы, которые используют несколько репозиториев:
type UserService struct {
postgresRepo PostgresRepository
mongoRepo MongoRepository
redisRepo RedisRepository
}
func NewUserService(postgres PostgresRepository, mongo MongoRepository, redis RedisRepository) *UserService {
return &UserService{
postgresRepo: postgres,
mongoRepo: mongo,
redisRepo: redis,
}
}
func (s *UserService) GetUserProfile(userID uint) (*UserProfile, error) {
// Получаем данные из PostgreSQL
user, err := s.postgresRepo.FindUser(userID)
if err != nil {
return nil, err
}
// Получаем доп. данные из MongoDB
profileData, err := s.mongoRepo.FindDocument("profiles", bson.M{"user_id": userID})
if err != nil {
return nil, err
}
// Кэшируем в Redis
s.redisRepo.Set(fmt.Sprintf("user:%d", userID), profileData, time.Hour)
// Составляем объединенный профиль
return &UserProfile{
User: user,
Profile: profileData,
}, nil
}
Практическая реализация контейнера зависимостей
Вот полная реализация контейнера зависимостей с ленивой инициализацией:
package di
import (
"context"
"log"
"time"
"github.com/go-redis/redis/v8"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type Container struct {
config *Config
logger Logger
postgresRepo PostgresRepository
mongoRepo MongoRepository
redisRepo RedisRepository
}
type Config struct {
Database DatabaseConfig
MongoDB MongoDBConfig
Redis RedisConfig
}
type DatabaseConfig struct {
URL string
}
type MongoDBConfig struct {
URI string
}
type RedisConfig struct {
Address string
}
type Logger interface {
Fatal(msg string, fields ...interface{})
}
type PostgresRepository interface {
FindUser(id uint) (*User, error)
CreateUser(user *User) error
// Другие методы для PostgreSQL
}
type MongoRepository interface {
FindDocument(collection string, filter interface{}) (interface{}, error)
InsertDocument(collection string, document interface{}) error
// Другие методы для MongoDB
}
type RedisRepository interface {
Set(key string, value interface{}, expiration time.Duration) error
Get(key string) (string, error)
// Другие методы для Redis
}
func NewContainer(cfg *Config, log Logger) *Container {
return &Container{
config: cfg,
logger: log,
}
}
// PostgreSQL
func (c *Container) Postgres() PostgresRepository {
if c.postgresRepo == nil {
db, err := gorm.Open(postgres.Open(c.config.Database.URL), &gorm.Config{})
if err != nil {
c.logger.Fatal("Failed to connect to PostgreSQL", "error", err)
}
c.postgresRepo = &postgresRepository{db: db}
}
return c.postgresRepo
}
// MongoDB
func (c *Container) Mongo() MongoRepository {
if c.mongoRepo == nil {
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(c.config.MongoDB.URI))
if err != nil {
c.logger.Fatal("Failed to connect to MongoDB", "error", err)
}
c.mongoRepo = &mongoRepository{client: client}
}
return c.mongoRepo
}
// Redis
func (c *Container) Redis() RedisRepository {
if c.redisRepo == nil {
rdb := redis.NewClient(&redis.Options{
Addr: c.config.Redis.Address,
})
c.redisRepo = &redisRepository{client: rdb}
}
return c.redisRepo
}
// Закрытие всех соединений
func (c *Container) Close() error {
var errors []error
if c.redisRepo != nil {
if err := c.redisRepo.(*redisRepository).client.Close(); err != nil {
errors = append(errors, err)
}
}
if c.mongoRepo != nil {
if err := c.mongoRepo.(*mongoRepository).client.Disconnect(context.Background()); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return errors[0] // Возвращаем первую ошибку
}
return nil
}
Рекомендации по организации кода
1. Структура проекта
/project
/cmd
/server
main.go
/internal
/config
config.go
/di
container.go
/repositories
/postgres
repository.go
user_repository.go
/mongo
repository.go
document_repository.go
/redis
repository.go
cache_repository.go
/services
user_service.go
document_service.go
/models
user.go
document.go
/pkg
/database
postgres.go
mongo.go
redis.go
2. Правильное тестирование
Используйте моки для тестирования сервисов:
type MockPostgresRepository struct {
users map[uint]*User
}
func (m *MockPostgresRepository) FindUser(id uint) (*User, error) {
user, exists := m.users[id]
if !exists {
return nil, gorm.ErrRecordNotFound
}
return user, nil
}
func (m *MockPostgresRepository) CreateUser(user *User) error {
m.users[user.ID] = user
return nil
}
// Использование в тесте
func TestUserService_GetUser(t *testing.T) {
mockPostgres := &MockPostgresRepository{
users: map[uint]*User{
1: {ID: 1, Name: "Test User"},
},
}
userService := NewUserService(mockPostgres, nil, nil)
user, err := userService.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Test User", user.Name)
}
3. Конфигурация
Используйте структурированную конфигурацию:
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
MongoDB MongoDBConfig `yaml:"mongodb"`
Redis RedisConfig `yaml:"redis"`
}
type ServerConfig struct {
Port int `yaml:"port"`
}
// Загрузка конфигурации
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
Выводы и лучшие практики
Ключевые рекомендации:
- Используйте отдельные интерфейсы для каждой базы данных вместо одного большого интерфейса
- Внедряйте зависимости через контейнер с ленивой инициализацией
- Следуйте принципу разделения интерфейсов (Interface Segregation Principle)
- Тестируйте каждый репозиторий отдельно с использованием моков
- Используйте сервисный слой для бизнес-логики, которая объединяет данные из разных источников
Почему ваш текущий подход может быть неоптимальным:
- Тесная связанность: Сервисы, которым нужен только Redis, будут вынуждены зависеть от интерфейса, включающего MongoDB и PostgreSQL
- Усложненное тестирование: Тестирование сервисов становится сложнее из-за необходимости мокировать все три репозитория
- Проблемы с масштабированием: При добавлении новой базы данных придется изменять основной интерфейс
Альтернативные подходы для рассмотрения:
- Фабричный подход: Создайте фабрику для репозиториев
- Адаптеры: Используйте паттерн адаптеров для унификации интерфейсов
- Контекстные зависимости: Передавайте необходимые репозитории через контекст HTTP запроса
According to the Three Dots Labs blog, “The repository pattern works pretty nicely with Golang” when properly implemented with separate interfaces for different database types.
Правильная структура внедрения зависимостей с несколькими базами данных в Go должна быть гибкой, тестируемой и легко расширяемой. Разделение интерфейсов и ленивая инициализация через контейнер зависимостей обеспечивают эти качества.