From f256e36588812cba4eefd897f89962025868687d Mon Sep 17 00:00:00 2001 From: Alex Geer Date: Thu, 7 Jul 2022 16:16:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BF=D0=BE=D0=B4=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BA=20SQL?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B8=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/component/bootstrap/bootstrap.go | 10 +- .../component/migration_sql/migration_sql.go | 101 ++++++++ module/db/sql/dsn.go | 70 ++++++ module/db/sql/errors.go | 77 ++++++ module/db/sql/gist.go | 26 ++ module/db/sql/logger_gorm.go | 59 +++++ module/db/sql/migration.go | 94 ++++++++ module/db/sql/sql.go | 228 ++++++++++++++++++ module/db/sql/types.go | 77 ++++++ module/db/sql/types/types.go | 70 ++++++ module/db/sql/types_errors.go | 50 ++++ types/db/sql.go | 9 + 12 files changed, 866 insertions(+), 5 deletions(-) create mode 100644 application/component/migration_sql/migration_sql.go create mode 100644 module/db/sql/dsn.go create mode 100644 module/db/sql/errors.go create mode 100644 module/db/sql/gist.go create mode 100644 module/db/sql/logger_gorm.go create mode 100644 module/db/sql/migration.go create mode 100644 module/db/sql/sql.go create mode 100644 module/db/sql/types.go create mode 100644 module/db/sql/types/types.go create mode 100644 module/db/sql/types_errors.go create mode 100644 types/db/sql.go diff --git a/application/component/bootstrap/bootstrap.go b/application/component/bootstrap/bootstrap.go index 316827b..93f86d3 100644 --- a/application/component/bootstrap/bootstrap.go +++ b/application/component/bootstrap/bootstrap.go @@ -41,20 +41,20 @@ func (brp *impl) Preferences() kitTypes.ComponentPreferences { cEnvironment = `(?mi)application/component/environment$` cInterrupt = `(?mi)application/component/interrupt$` cConfiguration = `(?mi)application/component/configuration$` - cLogging = `(?mi)application/component/logging$` - cLoggerLogrus = `(?mi)application/component/logger_logrus$` + cLogging = `(?mi)application/component/logg.*` + cLoggerConsole = `(?mi)application/component/logger_console$` cPidfile = `(?mi)application/component/pidfile$` - cMigrations = `(?mi)application/component/migrations$` + cMigration = `(?mi)application/component/migration.*$` ) return kitTypes.ComponentPreferences{ After: []string{ cEnvironment, cConfiguration, cLogging, - cLoggerLogrus, + cLoggerConsole, cInterrupt, cPidfile, - cMigrations, + cMigration, }, } } diff --git a/application/component/migration_sql/migration_sql.go b/application/component/migration_sql/migration_sql.go new file mode 100644 index 0000000..b0cb436 --- /dev/null +++ b/application/component/migration_sql/migration_sql.go @@ -0,0 +1,101 @@ +// Package migration_sql +package migration_sql + +import ( + kitModuleCfg "github.com/webnice/kit/v3/module/cfg" + kitModuleCfgReg "github.com/webnice/kit/v3/module/cfg/reg" + kitModuleDbSql "github.com/webnice/kit/v3/module/db/sql" + kitTypes "github.com/webnice/kit/v3/types" + kitTypesDb "github.com/webnice/kit/v3/types/db" +) + +// Структура объекта компоненты. +type impl struct { + cfg kitModuleCfg.Interface + databaseSql *kitTypesDb.DatabaseSqlConfiguration +} + +// Регистрация компоненты в приложении. +func init() { kitModuleCfgReg.Registration(newComponent()) } + +// Конструктор объекта компоненты. +func newComponent() kitTypes.Component { + var m8s = &impl{ + cfg: kitModuleCfg.Get(), + databaseSql: new(kitTypesDb.DatabaseSqlConfiguration), + } + + m8s.registrationConfigurationError(m8s.cfg.Gist().ConfigurationRegistration(m8s.databaseSql)) + + return m8s +} + +// Ссылка на менеджер логирования, для удобного использования внутри компоненты или модуля. +func (m8s *impl) log() kitTypes.Logger { return m8s.cfg.Log() } + +// Обработка ошибки регистрации конфигурации. +func (m8s *impl) registrationConfigurationError(err error) { + if err == nil { + return + } + switch eto := err.(type) { + case kitModuleCfg.Err: + m8s.cfg.Gist().ErrorAppend(eto) + default: + m8s.cfg.Gist().ErrorAppend(m8s.cfg.Errors().ConfigurationApplicationObject(0, eto)) + } +} + +// Preferences Функция возвращает настройки компоненты. +func (m8s *impl) Preferences() kitTypes.ComponentPreferences { + const ( + cEnvironment = `(?mi)application/component/environment$` + cInterrupt = `(?mi)application/component/interrupt$` + cConfiguration = `(?mi)application/component/configuration$` + cLogging = `(?mi)application/component/logg.*` + cLoggerConsole = `(?mi)application/component/logger_console$` + cPidfile = `(?mi)application/component/pidfile$` + cBootstrap = `(?mi)application/component/bootstrap$` + ) + return kitTypes.ComponentPreferences{ + After: []string{cConfiguration, cLoggerConsole, cLogging, cPidfile, cInterrupt, cEnvironment}, + Require: []string{cPidfile}, + Before: []string{cBootstrap}, + } +} + +// Initiate Функция инициализации компонента и подготовки компонента к запуску. +func (m8s *impl) Initiate() (err error) { + var ( + elm interface{} + ok bool + c *kitTypesDb.DatabaseSqlConfiguration + ) + + // Загрузка конфигурации базы данных, сохранённой в конфигурации приложения. + if elm, err = m8s.cfg.ConfigurationByObject(m8s.databaseSql); err != nil { + return + } + // Приведение пустого интерфейса к типу данных. + if c, ok = elm.(*kitTypesDb.DatabaseSqlConfiguration); ok { + // Исправление пути к миграции на абсолютный путь, исправление по адресу, поэтому все кто запросят + // конфигурацию базы данных, получат исправленный вариант. + m8s.cfg.Gist().AbsolutePathAndUpdate(&c.SqlDB.Migration) + // Обновление локальной копии конфигурации, так как после работы yaml библиотеки может слетать адрес. + m8s.databaseSql = c + } + + return +} + +// Do Выполнение компонента приложения. +func (m8s *impl) Do() (levelDone bool, levelExit bool, err error) { + if err = kitModuleDbSql.Get().MigrationUp(); err != nil { + levelDone, levelExit = true, true + } + + return +} + +// Finalize Функция вызывается перед завершением компонента и приложения в целом. +func (m8s *impl) Finalize() (err error) { return } diff --git a/module/db/sql/dsn.go b/module/db/sql/dsn.go new file mode 100644 index 0000000..0cc971e --- /dev/null +++ b/module/db/sql/dsn.go @@ -0,0 +1,70 @@ +// Package sql +package sql + +import ( + "fmt" + "strings" +) + +// Создание DSN для подключения к базе данных. +func (mys *impl) makeDsn() (err error) { + const keyTcp, keySocket = `tcp`, `socket` + var ( + n int + found bool + ) + + // Проверка конфигурации. + if mys.cfg == nil { + err = mys.Errors().ConfigurationIsEmpty(0) + return + } + // Проверка драйвера базы данных. + for n = range supportDrivers { + if strings.EqualFold(mys.cfg.Driver, supportDrivers[n]) { + mys.cfg.Driver, found = supportDrivers[n], true + break + } + } + if !found { + err = mys.Errors().UnknownDatabaseDriver(0, mys.cfg.Driver) + return + } + // Самая простая конфигурация: sqlite + if mys.cfg.Driver == driverSqlite { + mys.dsn = fmt.Sprintf("%s?%s", mys.cfg.Name, dsnTimeSettings) + return + } + // Имя пользователя. + if mys.cfg.Login == "" { + err = mys.Errors().UsernameIsEmpty(0) + return + } + // Имя пользователя и пароль можно добавлять в DSN. + mys.dsn = fmt.Sprintf("%s:%s", mys.cfg.Login, mys.cfg.Password) + // Тип подключения. + switch strings.ToLower(mys.cfg.Type) { + case keyTcp: + mys.dsn += fmt.Sprintf("@%s(%s:%d)", keyTcp, mys.cfg.Host, mys.cfg.Port) + case keySocket: + mys.dsn += fmt.Sprintf(dsnUnixTpl, mys.cfg.Socket) + default: + err = mys.Errors().WrongConnectionType(0, mys.cfg.Type) + return + } + mys.cfg.Type = strings.ToLower(mys.cfg.Type) + // Название базы данных. + mys.dsn += fmt.Sprintf("/%s", mys.cfg.Name) + // Парсинг времени. + mys.dsn += fmt.Sprintf(dsnTimeSettings, mys.cfg.ParseTime) + // Зона времени. + if mys.cfg.TimezoneLocation != "" { + mys.dsn += fmt.Sprintf(dsnLocationSettings, mys.cfg.TimezoneLocation) + } + // Кодировка соединения с базой данных. + if mys.cfg.Charset != "" { + mys.dsn += fmt.Sprintf(dsnCharsetTpl, mys.cfg.Charset) + } + + return +} diff --git a/module/db/sql/errors.go b/module/db/sql/errors.go new file mode 100644 index 0000000..1ad1694 --- /dev/null +++ b/module/db/sql/errors.go @@ -0,0 +1,77 @@ +// Package sql +package sql + +// Обычные ошибки +const ( + eConfigurationIsEmpty uint = iota + 1 // 001 + eUnknownDatabaseDriver // 002 + eUsernameIsEmpty // 003 + eWrongConnectionType // 004 + eConnectError // 005 + eDriverUnImplemented // 006 + eApplyMigration // 007 + eUnknownDialect // 008 +) + +// Текстовые значения кодов ошибок на основном языке приложения. +const ( + cConfigurationIsEmpty = `Конфигурация подключения к базе данных пустая.` + cUnknownDatabaseDriver = `Указан неизвестный или не поддерживаемый драйвер базы данных: ` + "%q." + cUsernameIsEmpty = `Не указано имя пользователя, для подключения к базе данных.` + cWrongConnectionType = `Указан неизвестный или не поддерживаемый способ подключения к базе данных: ` + "%q." + cConnectError = `Подключение к базе данных завершилось ошибкой: ` + "%s." + cDriverUnImplemented = `Подключение к базе данных с помощью драйвера %q не создано.` + cApplyMigration = `Применение новых миграций базы данных прервано ошибкой: ` + "%s." + cUnknownDialect = `Применение миграций базы данных, настройка диалекта %q прервано ошибкой: ` + "%s." +) + +// Константы указаны в объектах, адрес которых фиксирован всё время работы приложения. +// Это позволяет сравнивать ошибки между собой используя обычное сравнение "==", но сравнивать необходимо только +// якорь "Anchor()" объекта ошибки. +var ( + errSingleton = &Error{} + errConfigurationIsEmpty = err{tpl: cConfigurationIsEmpty, code: eConfigurationIsEmpty} + errUnknownDatabaseDriver = err{tpl: cUnknownDatabaseDriver, code: eUnknownDatabaseDriver} + errUsernameIsEmpty = err{tpl: cUsernameIsEmpty, code: eUsernameIsEmpty} + errWrongConnectionType = err{tpl: cWrongConnectionType, code: eWrongConnectionType} + errConnectError = err{tpl: cConnectError, code: eConnectError} + errDriverUnImplemented = err{tpl: cDriverUnImplemented, code: eDriverUnImplemented} + errApplyMigration = err{tpl: cApplyMigration, code: eApplyMigration} + errUnknownDialect = err{tpl: cUnknownDialect, code: eUnknownDialect} +) + +// ERRORS: Реализация ошибок с возможностью сравнения ошибок между собой. + +// ConfigurationIsEmpty Конфигурация подключения к базе данных пустая. +func (e *Error) ConfigurationIsEmpty(code uint) Err { return newErr(&errConfigurationIsEmpty, code) } + +// UnknownDatabaseDriver Указан неизвестный или не поддерживаемый драйвер базы данных: ... +func (e *Error) UnknownDatabaseDriver(code uint, driver string) Err { + return newErr(&errUnknownDatabaseDriver, code, driver) +} + +// UsernameIsEmpty Не указано имя пользователя, для подключения к базе данных. +func (e *Error) UsernameIsEmpty(code uint) Err { return newErr(&errUsernameIsEmpty, code) } + +// WrongConnectionType Указан неизвестный или не поддерживаемый способ подключения к базе данных: ... +func (e *Error) WrongConnectionType(code uint, connType string) Err { + return newErr(&errWrongConnectionType, code, connType) +} + +// ConnectError Подключение к базе данных завершилось ошибкой: ... +func (e *Error) ConnectError(code uint, err error) Err { return newErr(&errConnectError, code, err) } + +// DriverUnImplemented Подключение к базе данных с помощью драйвера ... не создано. +func (e *Error) DriverUnImplemented(code uint, driver string) Err { + return newErr(&errDriverUnImplemented, code, driver) +} + +// ApplyMigration Применение новых миграций базы данных прервано ошибкой: ... +func (e *Error) ApplyMigration(code uint, err error) Err { + return newErr(&errApplyMigration, code, err) +} + +// UnknownDialect Применение миграций базы данных, настройка диалекта ... прервано ошибкой: ... +func (e *Error) UnknownDialect(code uint, dialect string, err error) Err { + return newErr(&errUnknownDialect, code, dialect, err) +} diff --git a/module/db/sql/gist.go b/module/db/sql/gist.go new file mode 100644 index 0000000..509a143 --- /dev/null +++ b/module/db/sql/gist.go @@ -0,0 +1,26 @@ +// Package sql +package sql + +import ( + "github.com/jmoiron/sqlx" + "gorm.io/gorm" +) + +// Gist Возвращается настроенный и готовый к работе интерфейс подключения к базе данных. +func (db *Implementation) Gist() Interface { return db.getParent() } + +// Gorm Возвращается настроенный и готовый к работе объект ORM gorm.io/gorm. +func (db *Implementation) Gorm() *gorm.DB { return db.getParent().GormDB() } + +// Sqlx Настроенный и готовый к работе объект обёртки над соединением с БД github.com/jmoiron/sqlx. +func (db *Implementation) Sqlx() *sqlx.DB { return db.getParent().SqlxDB() } + +// Возвращает объект родителя, с запоминанием объекта. +func (db *Implementation) getParent() Interface { + if db.parent != nil { + return db.parent + } + db.parent = Get() + + return db.parent +} diff --git a/module/db/sql/logger_gorm.go b/module/db/sql/logger_gorm.go new file mode 100644 index 0000000..7ebcbb5 --- /dev/null +++ b/module/db/sql/logger_gorm.go @@ -0,0 +1,59 @@ +// Package sql +package sql + +import ( + "context" + "time" + + kitModuleDye "github.com/webnice/kit/v3/module/dye" + kitTypes "github.com/webnice/kit/v3/types" + + gormLogger "gorm.io/gorm/logger" +) + +func (mys *impl) LogMode(l gormLogger.LogLevel) gormLogger.Interface { + mys.log().Noticef("gorm уровень логирования: %d", int(l)) + return mys +} + +func (mys *impl) Info(_ context.Context, s string, i ...interface{}) { mys.log().Infof(s, i...) } + +func (mys *impl) Warn(_ context.Context, s string, i ...interface{}) { mys.log().Warningf(s, i...) } + +func (mys *impl) Error(_ context.Context, s string, i ...interface{}) { mys.log().Errorf(s, i...) } + +func (mys *impl) Trace(_ context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + const ( + keyQuery, keySql = `query`, `sql` + keyDriver, keyElapsed, keyRows = `driver`, `elapsed`, `rows` + tplTracef, tplErrorf = `sql:"%s"`, `sql:"%s", ошибка: %s` + ) + var ( + elapsed time.Duration + sql string + rows int64 + keys kitTypes.LoggerKey + ) + + elapsed = time.Since(begin) + sql, rows = fc() + keys = kitTypes.LoggerKey{ + keyQuery: keySql, + keyDriver: mys.cfg.Driver, + keyElapsed: elapsed, + keyRows: rows, + } + switch err { + case nil: + mys.log().Key(keys).Tracef( + tplTracef, + kitModuleDye.New().Yellow().Done().String()+sql+kitModuleDye.New().Normal().Done().String(), + ) + default: + mys.log().Key(keys).Errorf( + tplErrorf, + kitModuleDye.New().Yellow().Done().String()+sql+kitModuleDye.New().Reset().Done().String(), + kitModuleDye.New().Red().Done().String()+err.Error()+kitModuleDye.New().Reset().Done().String(), + ) + } +} diff --git a/module/db/sql/migration.go b/module/db/sql/migration.go new file mode 100644 index 0000000..dda2b8e --- /dev/null +++ b/module/db/sql/migration.go @@ -0,0 +1,94 @@ +// Package sql +package sql + +import ( + "math" + + //kitModuleCfg "github.com/webnice/kit/v3/module/cfg" + "github.com/webnice/migrate/goose" + + // Обязательно наличие зарегистрированных драйверов баз данных. + _ "github.com/go-sql-driver/mysql" // mysql + _ "github.com/jackc/pgx/v4" // postgre, cockroach, redshift + _ "github.com/mattn/go-sqlite3" // sqlite3 +) + +// MigrationUp Применение миграций базы данных. +func (mys *impl) MigrationUp() (err error) { + const ( + tplNewMigration = "Найдены новые миграции базы данных %q, новых миграций: %d." + tplNoNewMigration = "Нет новых миграций базы данных %q, версия базы данных: %d." + tplNewApply = "Применение миграций базы данных." + tplNewApplied = "Новые миграции базы данных %q успешно применены." + ) + var ( + migration goose.Migrations + next *goose.Migration + current int64 + n int + count int + end bool + ) + + // Соединение заблокировано, если оно находится в состоянии подключения или переподключения, необходимо подождать. + mys.connectMux.RLock() + mys.connectMux.RUnlock() + // Отключение таймаута на время применения миграций. + mys.connect.SetConnMaxLifetime(0) + defer func() { mys.connect.SetConnMaxLifetime(mys.cfg.MaxLifetimeConn) }() + // Настройка диалекта библиотеки применения миграций. + if err = goose.SetDialect(mys.cfg.Driver); err != nil { + err = mys.Errors().UnknownDialect(0, mys.cfg.Driver, err) + return + } + //kitModuleCfg.Get().Gist().AbsolutePathAndUpdate(&mys.cfg.Migration) + // Получение текущей версии базы данных и подсчёт количества новых миграций. + switch current, err = goose.EnsureDBVersion(mys.conn()); err { + case nil: + case goose.ErrNoNextVersion: + err = nil + default: + return + } + // Поиск миграций в папке миграций. + if migration, err = goose. + CollectMigrations(mys.cfg.Migration, 0, math.MaxInt64); err != nil { + return + } + for n = range migration { + if migration[n].Version > current { + count++ + } + } + if count <= 0 { + mys.log().Infof(tplNoNewMigration, mys.cfg.Driver, current) + return + } + mys.log().Infof(tplNewMigration, mys.cfg.Driver, count) + // Применение миграций. + mys.log().Info(tplNewApply) + for { + if end { + break + } + if current, err = goose.GetDBVersion(mys.conn()); err != nil { + end, err = true, nil + continue + } + switch next, err = migration.Next(current); err { + case nil: + case goose.ErrNoNextVersion: + end, err = true, nil + continue + } + if err = next.Up(mys.conn()); err != nil { + end, err = true, mys.Errors().ApplyMigration(0, err) + continue + } + } + if err == nil { + mys.log().Infof(tplNewApplied, mys.cfg.Driver) + } + + return +} diff --git a/module/db/sql/sql.go b/module/db/sql/sql.go new file mode 100644 index 0000000..f7088e6 --- /dev/null +++ b/module/db/sql/sql.go @@ -0,0 +1,228 @@ +// Package sql +package sql + +import ( + "database/sql" + "runtime" + "sync" + + kitModuleCfg "github.com/webnice/kit/v3/module/cfg" + kitTypes "github.com/webnice/kit/v3/types" + kitTypesDb "github.com/webnice/kit/v3/types/db" + + // Библиотеки работы с базой данных. + "github.com/jmoiron/sqlx" + "gorm.io/gorm" + + // Драйверы базы данных. + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" +) + +// Get Возвращается интерфейс для работы с базой данных. +// Если база данных доступна, тогда возвращается полностью настроенное и готовое к работе соединение с базой данных. +// Если база данных не доступна, тогда возвращается объект, методы которого заблокированы до момента установки +// соединения с базой данных. Параллельно запущен процесс подключения к базе данных, по окончании которого, +// блокировка методов объекта снимается. +func Get() Interface { + if singleton != nil { + return singleton + } + singleton = constructor() + + return singleton +} + +// Free Освобождает соединение работы с базой данных. +// Объект работы с базой данных полностью удаляется из памяти. +func Free() { singleton = nil } + +// Создание нового объекта подключения к базе данных. +func constructor() (mys *impl) { + var onDone chan struct{} + + mys = &impl{ + databaseSql: new(kitTypesDb.DatabaseSqlConfiguration), + connectMux: new(sync.RWMutex), + } + if mys.error = kitModuleCfg.Get(). + ConfigurationCopyByObject(mys.databaseSql); mys.error != nil { + return mys + } + mys.cfg = &mys.databaseSql.SqlDB + // Запуск подключения в отдельном процессе. + onDone = make(chan struct{}) + go mys.makeConnect(onDone) + // Ожидание запуска процесса подключения и настройки соединения. + <-onDone + close(onDone) + runtime.SetFinalizer(mys, destructor) + + return +} + +// Деструктор объекта. Закрывает соединение с базой данных. +func destructor(mys *impl) { + if mys.connect == nil { + return + } + _ = mys.Close() +} + +// Выполнение подключения к базе данных и настройки соединения. +func (mys *impl) makeConnect(onDone chan<- struct{}) { + var err error + + mys.connectMux.Lock() + defer mys.connectMux.Unlock() + onDone <- struct{}{} + // Создание DSN для подключения к базе данных. + if mys.error = mys.makeDsn(); mys.error != nil { + return + } + // Выполнение подключения к базе данных. + if mys.connect, err = sql.Open(mys.cfg.Driver, mys.dsn); err != nil { + mys.error = mys.Errors().ConnectError(0, err) + return + } + // Настройка подключения к базе данных. + mys.connect.SetConnMaxIdleTime(mys.cfg.MaxIdleTimeConn) + mys.connect.SetConnMaxLifetime(mys.cfg.MaxLifetimeConn) + mys.connect.SetMaxIdleConns(mys.cfg.MaxIdleConn) + mys.connect.SetMaxOpenConns(mys.cfg.MaxOpenConn) +} + +// Close Закрытие соединения с базой данных. +func (mys *impl) Close() (err error) { + mys.connectMux.Lock() + defer mys.connectMux.Unlock() + if mys.connect == nil { + return + } + err = mys.connect.Close() + mys.connect = nil + + return +} + +// Возвращает соединение с базой данных, если соединение отсутствовало, восстанавливает его. +func (mys *impl) conn() *sql.DB { + var onDone chan struct{} + + if mys.connect != nil { + return mys.connect + } + // Запуск подключения в отдельном процессе. + onDone = make(chan struct{}) + go mys.makeConnect(onDone) + // Ожидание запуска процесса подключения и настройки соединения. + <-onDone + close(onDone) + mys.connectMux.RLock() + mys.connectMux.RUnlock() + + return mys.connect +} + +// Возвращает последнюю ошибку работы с соединением базы данных с проверкой блокировки. +func (mys *impl) err() error { + mys.connectMux.RLock() + defer mys.connectMux.RUnlock() + return mys.error +} + +// Ссылка на менеджер логирования, для удобного использования внутри компоненты или модуля. +func (mys *impl) log() kitTypes.Logger { return kitModuleCfg.Get().Log() } + +// Errors Справочник всех ошибок пакета. +func (mys *impl) Errors() *Error { return Errors() } + +// E Ошибка соединения с базой данных. +// Если err==nil - база данных доступна, соединение активно, ошибок нет. +// Если err!=nil - есть проблема с соединением с базой данных. +func (mys *impl) E() (err error) { + if err = mys.err(); err != nil { + return + } + if err = mys.conn().Ping(); err != nil { + return + } + + return +} + +// Status Возвращает состояние подключения к базе данных. +func (mys *impl) Status() (ret *sql.DBStats) { + var stats sql.DBStats + + stats = mys.conn().Stats() + ret = &stats + + return +} + +// SqlDB Настроенный и готовый к работе бассейн соединений database/sql. +// Если возвращается nil - есть ошибки, ошибка доступна в функции E() +func (mys *impl) SqlDB() (ret *sql.DB) { ret = mys.conn(); return } + +// GormDB Настроенный и готовый к работе объект ORM gorm.io/gorm. +// Если возвращается nil - есть ошибки, ошибка доступна в функции E() +func (mys *impl) GormDB() (ret *gorm.DB) { + if mys.err() != nil { + return + } + switch mys.cfg.Driver { + case driverMySQL: + ret, mys.error = gorm.Open(mysql.New(mysql.Config{ + DriverName: mys.cfg.Driver, + DSN: mys.dsn, + Conn: mys.conn(), + DefaultStringSize: mys.cfg.DefaultStringSize, + DisableDatetimePrecision: mys.cfg.DisableDatetimePrecision, + }), &gorm.Config{ + SkipDefaultTransaction: mys.cfg.SkipDefaultTransaction, + DisableAutomaticPing: mys.cfg.DisableAutomaticPing, + PrepareStmt: mys.cfg.PrepareStmt, + CreateBatchSize: mys.cfg.CreateBatchSize, + Logger: mys, + }) + case driverPostgreSQL: + ret, mys.error = gorm.Open(postgres.New(postgres.Config{ + DriverName: mys.cfg.Driver, + DSN: mys.dsn, + Conn: mys.conn(), + PreferSimpleProtocol: mys.cfg.PostgreSQLPreferSimpleProtocol, + }), &gorm.Config{ + SkipDefaultTransaction: mys.cfg.SkipDefaultTransaction, + DisableAutomaticPing: mys.cfg.DisableAutomaticPing, + PrepareStmt: mys.cfg.PrepareStmt, + CreateBatchSize: mys.cfg.CreateBatchSize, + Logger: mys, + }) + case driverSqlite: + ret, mys.error = gorm.Open(sqlite.Open(mys.dsn), &gorm.Config{ + SkipDefaultTransaction: mys.cfg.SkipDefaultTransaction, + DisableAutomaticPing: mys.cfg.DisableAutomaticPing, + PrepareStmt: mys.cfg.PrepareStmt, + CreateBatchSize: mys.cfg.CreateBatchSize, + Logger: mys, + }) + default: + mys.error = mys.Errors().DriverUnImplemented(0, mys.cfg.Driver) + return + } + + return +} + +// SqlxDB Настроенный и готовый к работе объект обёртки над соединением с БД github.com/jmoiron/sqlx. +// Если возвращается nil - есть ошибки, ошибка доступна в функции E() +func (mys *impl) SqlxDB() (ret *sqlx.DB) { + if mys.err() != nil { + return + } + ret = sqlx.NewDb(mys.conn(), mys.cfg.Driver) + + return +} diff --git a/module/db/sql/types.go b/module/db/sql/types.go new file mode 100644 index 0000000..d2fb61d --- /dev/null +++ b/module/db/sql/types.go @@ -0,0 +1,77 @@ +// Package sql +package sql + +import ( + "database/sql" + "sync" + + kitModuleDbSqlTypes "github.com/webnice/kit/v3/module/db/sql/types" + kitTypesDb "github.com/webnice/kit/v3/types/db" + + "github.com/jmoiron/sqlx" + "gorm.io/gorm" +) + +const ( + driverMySQL = `mysql` + driverPostgreSQL = `postgres` + driverSqlite = `sqlite` + dsnTimeSettings = `?parseTime=%t` + dsnLocationSettings = `&loc=%s` + dsnUnixTpl = `@unix(%s)` + dsnCharsetTpl = `&charset=%s` +) + +var ( + singleton *impl + supportDrivers = []string{driverMySQL, driverPostgreSQL, driverSqlite} +) + +// Interface Интерфейс пакета. +type Interface interface { + // Close Закрытие соединения с базой данных. + Close() (err error) + + // E Ошибка соединения с базой данных. + // Если err==nil - база данных доступна, соединение активно, ошибок нет. + // Если err!=nil - есть проблема с соединением с базой данных. + E() (err error) + + // Status Возвращает состояние подключения к базе данных. + Status() (ret *sql.DBStats) + + // SqlDB Настроенный и готовый к работе бассейн соединений database/sql. + // Если возвращается nil - есть ошибки, ошибка доступна в функции E() + SqlDB() (ret *sql.DB) + + // GormDB Настроенный и готовый к работе объект ORM gorm.io/gorm. + // Если возвращается nil - есть ошибки, ошибка доступна в функции E() + GormDB() (ret *gorm.DB) + + // SqlxDB Настроенный и готовый к работе объект обёртки над соединением с БД github.com/jmoiron/sqlx. + // Если возвращается nil - есть ошибки, ошибка доступна в функции E() + SqlxDB() (ret *sqlx.DB) + + // MigrationUp Применение миграций базы данных. + MigrationUp() (err error) + + // ОШИБКИ + + // Errors Справочник всех ошибок пакета. + Errors() *Error +} + +// Объект сущности, реализующий интерфейс Interface. +type impl struct { + databaseSql *kitTypesDb.DatabaseSqlConfiguration // Конфигурация для подключения к базе данных. + cfg *kitModuleDbSqlTypes.Configuration // Сегмент конфигурации для подключения к базе данных. + dsn string // Строка в формате DSN для подключения к базе данных. + error error // Последняя ошибка, возникшая при работе с соединением или драйвером базы данных. + connect *sql.DB // Установленное соединение с базой данных. + connectMux *sync.RWMutex // Блокировка доступа на время установки соединения или переподключения. +} + +// Implementation Встраиваемая структура в модель базы данных, для лёгкого подключения "по требованию" к базе данных. +type Implementation struct { + parent Interface // Временная копия родительского объекта подключения к базе данных. +} diff --git a/module/db/sql/types/types.go b/module/db/sql/types/types.go new file mode 100644 index 0000000..4b18add --- /dev/null +++ b/module/db/sql/types/types.go @@ -0,0 +1,70 @@ +// Package types +package types + +import "time" + +// Configuration SQL database configuration structure. +type Configuration struct { + // Driver Драйвер. + Driver string `yaml:"Driver" default-value:"mysql"` + // Host Хост базы данных. + Host string `yaml:"Host" default-value:"localhost"` + // Port Порт подключения по протоколу tcp/ip. + Port int16 `yaml:"Port" default-value:"3306"` + // Type Тип подключения к базе данных socket | tcp. + Type string `yaml:"Type" default-value:"tcp"` + // Socket Путь к socket файлу. + Socket string `yaml:"Socket" default-value:"-"` + // Name Имя базы данных. + Name string `yaml:"Name" default-value:"database"` + // Login Логин к базе данных. + Login string `yaml:"Login" default-value:"root"` + // Password Пароль к базе данных. + Password string `yaml:"Password" default-value:"-"` + // Migration Путь к папке с файлами миграций базы данных. + Migration string `yaml:"Migration" default-value:"-"` + + // Настройки соединения и библиотек. + + // Charset Кодировка данных. + Charset string `yaml:"Charset" default-value:"utf8"` + // ParseTime Парсинг значений даты и времени. + ParseTime bool `yaml:"ParseTime" default-value:"true"` + // TimezoneLocation Зона времени по умолчанию. + TimezoneLocation string `yaml:"TimezoneLocation" default-value:"Local"` + // DefaultStringSize Размер значений по умолчанию для строковых колонок. + DefaultStringSize uint `yaml:"DefaultStringSize" default-value:"256"` + // CreateBatchSize Размер пакета групповой вставки по умолчанию. + CreateBatchSize int `yaml:"CreateBatchSize" default-value:"100"` + // DisableDatetimePrecision Отключить точность datetime колонок для совместимости с MySQL 5.6 и более старой. + DisableDatetimePrecision bool `yaml:"DisableDatetimePrecision" default-value:"false"` + // MaxIdleConn Максимальное количество соединений в пуле бездействия. + MaxIdleConn int `yaml:"MaxIdleConn" default-value:"10"` + // MaxOpenConn Максимальное количество открытых соединений с БД. + MaxOpenConn int `yaml:"MaxOpenConn" default-value:"20"` + // MaxIdleTimeConn Время ожидания не используемого соединения перед закрытием. + MaxIdleTimeConn time.Duration `yaml:"MaxIdleTimeConn" default-value:"5m"` + // MaxLifetimeConn Максимальное время повторного использования соединения. + MaxLifetimeConn time.Duration `yaml:"MaxLifetimeConn" default-value:"1h"` + // SkipDefaultTransaction Не создавать транзакции для запросов к базе данных. + SkipDefaultTransaction bool `yaml:"SkipDefaultTransaction" default-value:"false"` + // DisableAutomaticPing Отключает автоматический пинг перед запросом к базе данных. + DisableAutomaticPing bool `yaml:"DisableAutomaticPing" default-value:"false"` + // PrepareStmt Включается подготовка данных и кеширование их при выполнении любого SQL запроса. + PrepareStmt bool `yaml:"PrepareStmt" default-value:"false"` + // PostgreSQLPreferSimpleProtocol отключает неявное использование подготовленных инструкций. + // По умолчанию pgx автоматически использует расширенный протокол. Это может повысить производительность за + // счёт возможности использования двоичного формата. Он также не зависит от очистки параметров на стороне клиента. + // Так же, он требует двух обходов для каждого запроса (если не используется подготовленный оператор) и может быть + // несовместимым с прокси-серверами, такими как PGBouncer. + // Установка параметра PostgreSQLPreferSimpleProtocol=true приводит к тому, что по умолчанию используется простой + // протокол. + PostgreSQLPreferSimpleProtocol bool `yaml:"PostgreSQLPreferSimpleProtocol" default-value:"true"` + + // Логирование запросов. + + // TODO: Сделать отдельное детальное и удобное для анализа, логирование запросов к базе данных, но потом... + + // Loglevel Уровень логирования SQL запросов. + //Loglevel kmll.Level `yaml:"Loglevel" default-value:"notice"` +} diff --git a/module/db/sql/types_errors.go b/module/db/sql/types_errors.go new file mode 100644 index 0000000..31004a9 --- /dev/null +++ b/module/db/sql/types_errors.go @@ -0,0 +1,50 @@ +// Package sql +package sql + +import "fmt" + +type ( + // Error Объект-одиночка со списком ошибок которые можно сравнивать по якорю через '=='. + Error struct{} + + // Внутренняя структура объекта ошибки с кодом, шаблоном, якорем и интерфейсом error. + err struct { + tpl string // Шаблон ошибки. + code uint // Код ошибки. + args []interface{} // Иные аргументы ошибки. + anchor error // Константа ошибки с фиксированным адресом. + errFn func() string // Функция интерфейса error. + } + + // Err Интерфейс ошибки приложения. + Err interface { + Anchor() error // Якорь, по которому можно сравнивать две ошибки между собой. + Code() uint // Код ошибки. + Error() string // Сообщение об ошибке или шаблон сообщения об ошибке. + } +) + +// Anchor Реализация интерфейса error для якоря сравнения. +func (err err) Anchor() error { return err.anchor } + +// Code Возврат кода ошибки. +func (err err) Code() uint { return err.code } + +// Error Реализация интерфейса error. +func (err err) Error() string { return err.errFn() } + +// Errors Справочник всех ошибок пакета. +func Errors() *Error { return errSingleton } + +// Конструктор объекта ошибки. +func newErr(obj *err, code uint, arg ...interface{}) Err { + if code == 0 { + code = obj.code // Если код ошибки не изменён, используется код ошибки из шаблона. + } + return &err{ + anchor: obj, + code: code, + args: arg, + errFn: func() string { return fmt.Sprintf(obj.tpl, arg...) }, + } +} diff --git a/types/db/sql.go b/types/db/sql.go new file mode 100644 index 0000000..f0e93c7 --- /dev/null +++ b/types/db/sql.go @@ -0,0 +1,9 @@ +// Package db +package db + +import kitModuleDbSqlTypes "github.com/webnice/kit/v3/module/db/sql/types" + +// DatabaseSqlConfiguration Конфигурация подключения к базе данных SQL. +type DatabaseSqlConfiguration struct { + SqlDB kitModuleDbSqlTypes.Configuration `yaml:"SqlDB"` +}