DB optimization: pool, golang-migrate, consolidate to single Postgres

- Fix DB_NAME=db_yal -> mydb in api_yal .env
- Add connection pool (MaxOpenConns 25, MaxIdleConns 10, ConnMaxLifetime 30m)
- Replace GORM AutoMigrate with golang-migrate in api_yal and api_bb
- Create embedded SQL migrations for both APIs
- Add DB_SCHEMA support to api_bb config
- Consolidate to single Postgres: db_bb -> schema 'bb' on db container
- Remove db_bb service, bb-network, db_bb volume from compose
- Remove api_tp targets from Makefile
- Clean up old migrate.go
This commit is contained in:
valitovgaziz
2026-06-12 10:47:41 +05:00
parent ec83b97c25
commit b0350abfbe
18 changed files with 610 additions and 206 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ DB_HOST=db_tp
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=db_yal
DB_NAME=mydb
APP_PORT=8787
JWT_SECRET=secret
UPLOAD_PATH=./storage/uploads
+5
View File
@@ -5,6 +5,7 @@ go 1.25.0
require (
github.com/go-chi/chi/v5 v5.2.5
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-migrate/migrate/v4 v4.18.2
golang.org/x/crypto v0.49.0
gorm.io/gorm v1.31.1
)
@@ -13,12 +14,16 @@ require (
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
@@ -2,13 +2,19 @@ package database
import (
"api_yal/internal/config"
"api_yal/internal/models"
"api_yal/internal/logger"
"api_yal/migrations"
"database/sql"
"fmt"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"api_yal/internal/logger"
)
func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) {
@@ -23,45 +29,47 @@ func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
zapLogger.Info("AutoMigrate models")
// Автомиграция
if err := autoMigrate(db); err != nil {
zapLogger.Error("can't migrate models, error = %s", zap.Error(err))
return nil, fmt.Errorf("can't migrate models, error = %s", err)
zapLogger.Info("Configure connection pool")
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
zapLogger.Info("Migrate complite successfully")
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
zapLogger.Info("Fill init data")
zapLogger.Info("Run database migrations")
if err := runMigrations(sqlDB); err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
}
zapLogger.Info("Migrations completed successfully")
zapLogger.Info("Successfully connected to database")
zapLogger.Info("Successfully connected to database")
return db, nil
}
func autoMigrate(db *gorm.DB) error {
zapLogger := logger.Get()
zapLogger.Debug("Start migration")
models := []interface{}{
&models.Account{},
&models.UpdateHistory{},
&models.Object{},
&models.ObjectImage{},
&models.Amenity{},
&models.RatingVote{},
&models.VoteBreakdown{},
&models.Rating{},
&models.Feedback{},
&models.Comment{},
&models.Appeal{},
&models.AppealHistory{},
&models.PasswordReset{},
func runMigrations(sqlDB *sql.DB) error {
zapLogger := logger.Get()
source, err := iofs.New(migrations.FS, ".")
if err != nil {
return fmt.Errorf("failed to create migration source: %w", err)
}
for _, model := range models {
if err := db.AutoMigrate(model); err != nil {
return fmt.Errorf("failed to migrate %T: %w", model, err)
}
driver, err := postgres.WithInstance(sqlDB, &postgres.Config{})
if err != nil {
return fmt.Errorf("failed to create postgres driver: %w", err)
}
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
zapLogger.Error("Migration error", zap.Error(err))
return fmt.Errorf("failed to apply migrations: %w", err)
}
zapLogger.Debug("End migration seccessfully")
return nil
}
@@ -0,0 +1,14 @@
DROP TABLE IF EXISTS password_resets CASCADE;
DROP TABLE IF EXISTS appeal_histories CASCADE;
DROP TABLE IF EXISTS appeals CASCADE;
DROP TABLE IF EXISTS comments CASCADE;
DROP TABLE IF EXISTS feedbacks CASCADE;
DROP TABLE IF EXISTS rating_votes CASCADE;
DROP TABLE IF EXISTS vote_breakdowns CASCADE;
DROP TABLE IF EXISTS ratings CASCADE;
DROP TABLE IF EXISTS object_amenities CASCADE;
DROP TABLE IF EXISTS amenities CASCADE;
DROP TABLE IF EXISTS object_images CASCADE;
DROP TABLE IF EXISTS objects CASCADE;
DROP TABLE IF EXISTS update_histories CASCADE;
DROP TABLE IF EXISTS accounts CASCADE;
@@ -0,0 +1,227 @@
CREATE TABLE IF NOT EXISTS accounts (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255) NOT NULL DEFAULT 'Unknown',
first_name VARCHAR(255) NOT NULL DEFAULT 'FirstName',
last_name VARCHAR(255) NOT NULL DEFAULT 'LastName',
phone VARCHAR(255),
city VARCHAR(255),
organization_form VARCHAR(255),
organization_name VARCHAR(255),
organization_short VARCHAR(255),
inn VARCHAR(255),
personal_inn VARCHAR(255),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
role VARCHAR(255) NOT NULL DEFAULT 'user'
);
CREATE INDEX IF NOT EXISTS idx_accounts_deleted_at ON accounts(deleted_at);
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email);
CREATE TABLE IF NOT EXISTS update_histories (
id BIGSERIAL PRIMARY KEY,
model_id BIGINT,
model_type VARCHAR(255),
timestamp TIMESTAMPTZ,
updated_by BIGINT
);
CREATE INDEX IF NOT EXISTS idx_update_histories_updated_by ON update_histories(updated_by);
CREATE TABLE IF NOT EXISTS objects (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
owner_id BIGINT,
title VARCHAR(255) DEFAULT '',
short_name VARCHAR(255) NOT NULL,
long_name VARCHAR(255),
type VARCHAR(255),
price DECIMAL DEFAULT 0,
price_period VARCHAR(255) DEFAULT 'per_unit',
phone VARCHAR(255),
email VARCHAR(255),
site VARCHAR(255),
short_description VARCHAR(255),
description VARCHAR(255),
address VARCHAR(255),
latitude DECIMAL,
longitude DECIMAL,
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
status VARCHAR(255) DEFAULT 'active',
view_count BIGINT DEFAULT 0,
feedback_count BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_objects_deleted_at ON objects(deleted_at);
CREATE INDEX IF NOT EXISTS idx_objects_is_active ON objects(is_active);
CREATE TABLE IF NOT EXISTS object_images (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
object_id BIGINT NOT NULL,
url VARCHAR(255) NOT NULL,
is_primary BOOLEAN DEFAULT FALSE,
sort_order BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_object_images_deleted_at ON object_images(deleted_at);
CREATE INDEX IF NOT EXISTS idx_object_images_object_id ON object_images(object_id);
CREATE TABLE IF NOT EXISTS amenities (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
category VARCHAR(255),
icon VARCHAR(255),
description VARCHAR(255)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_amenities_name ON amenities(name);
CREATE TABLE IF NOT EXISTS object_amenities (
object_id BIGINT NOT NULL,
amenity_id BIGINT NOT NULL,
PRIMARY KEY (object_id, amenity_id)
);
CREATE TABLE IF NOT EXISTS ratings (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
owner_id BIGINT,
object_id BIGINT,
platform VARCHAR(255),
average_score DECIMAL,
total_votes BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_ratings_deleted_at ON ratings(deleted_at);
CREATE TABLE IF NOT EXISTS vote_breakdowns (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
rating_id BIGINT,
score_1 BIGINT DEFAULT 0,
score_2 BIGINT DEFAULT 0,
score_3 BIGINT DEFAULT 0,
score_4 BIGINT DEFAULT 0,
score_5 BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_vote_breakdowns_deleted_at ON vote_breakdowns(deleted_at);
CREATE TABLE IF NOT EXISTS rating_votes (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
platform VARCHAR(255),
target_id BIGINT,
voter_id BIGINT,
score BIGINT
);
CREATE INDEX IF NOT EXISTS idx_rating_votes_deleted_at ON rating_votes(deleted_at);
CREATE TABLE IF NOT EXISTS feedbacks (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
owner_id BIGINT NOT NULL,
object_id BIGINT NOT NULL,
platform VARCHAR(255),
score BIGINT,
comment_count BIGINT DEFAULT 0,
text VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS idx_feedbacks_deleted_at ON feedbacks(deleted_at);
CREATE INDEX IF NOT EXISTS idx_feedbacks_owner_id ON feedbacks(owner_id);
CREATE INDEX IF NOT EXISTS idx_feedbacks_object_id ON feedbacks(object_id);
CREATE TABLE IF NOT EXISTS comments (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
author_id BIGINT NOT NULL,
feedback_id BIGINT NOT NULL,
text VARCHAR(255) NOT NULL,
parent_id BIGINT,
is_edited BOOLEAN DEFAULT FALSE,
is_verified BOOLEAN DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_comments_deleted_at ON comments(deleted_at);
CREATE INDEX IF NOT EXISTS idx_comments_author_id ON comments(author_id);
CREATE INDEX IF NOT EXISTS idx_comments_feedback_id ON comments(feedback_id);
CREATE INDEX IF NOT EXISTS idx_comments_parent_id ON comments(parent_id);
CREATE TABLE IF NOT EXISTS appeals (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
author_id BIGINT,
type VARCHAR(255) NOT NULL,
status VARCHAR(255) NOT NULL DEFAULT 'new',
priority VARCHAR(255) NOT NULL DEFAULT 'medium',
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
attachments TEXT[],
contact_name VARCHAR(255),
contact_email VARCHAR(255),
contact_phone VARCHAR(255),
object_id BIGINT,
feedback_id BIGINT,
comment_id BIGINT,
ip_address VARCHAR(255),
user_agent VARCHAR(255),
assigned_to_id BIGINT,
resolved_at TIMESTAMPTZ,
resolved_by BIGINT,
resolution VARCHAR(255),
category VARCHAR(255),
labels TEXT[],
custom_data JSONB
);
CREATE INDEX IF NOT EXISTS idx_appeals_deleted_at ON appeals(deleted_at);
CREATE INDEX IF NOT EXISTS idx_appeals_author_id ON appeals(author_id);
CREATE INDEX IF NOT EXISTS idx_appeals_type ON appeals(type);
CREATE INDEX IF NOT EXISTS idx_appeals_status ON appeals(status);
CREATE INDEX IF NOT EXISTS idx_appeals_priority ON appeals(priority);
CREATE INDEX IF NOT EXISTS idx_appeals_object_id ON appeals(object_id);
CREATE INDEX IF NOT EXISTS idx_appeals_feedback_id ON appeals(feedback_id);
CREATE INDEX IF NOT EXISTS idx_appeals_comment_id ON appeals(comment_id);
CREATE INDEX IF NOT EXISTS idx_appeals_assigned_to_id ON appeals(assigned_to_id);
CREATE TABLE IF NOT EXISTS appeal_histories (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
appeal_id BIGINT NOT NULL,
user_id BIGINT,
old_status VARCHAR(255),
new_status VARCHAR(255),
comment VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS idx_appeal_histories_deleted_at ON appeal_histories(deleted_at);
CREATE INDEX IF NOT EXISTS idx_appeal_histories_appeal_id ON appeal_histories(appeal_id);
CREATE TABLE IF NOT EXISTS password_resets (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
account_id BIGINT NOT NULL,
token VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
used BOOLEAN DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_password_resets_deleted_at ON password_resets(deleted_at);
CREATE INDEX IF NOT EXISTS idx_password_resets_account_id ON password_resets(account_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_password_resets_token ON password_resets(token);
@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS users CASCADE;
DROP TABLE IF EXISTS reviews CASCADE;
DROP TABLE IF EXISTS news CASCADE;
DROP TABLE IF EXISTS authentications CASCADE;
DROP TABLE IF EXISTS filters CASCADE;
DROP TABLE IF EXISTS reports CASCADE;
@@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS