初始化实现

This commit is contained in:
Yun
2026-06-06 02:09:22 +08:00
commit 83c727372a
10 changed files with 670 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
# mysqlx Examples
本目录提供了 `mysqlx` SDK 的多种配置使用示例,按场景拆分到独立子目录:
- `basic_dsn/`:通过原始 DSN 字符串创建客户端。
- `expanded_dsn/`:通过 `WithDSNConfig` 及单个字段展开设置连接信息。
- `custom_dialector/`:使用自定义 `gorm.Dialector`(例如 SQLite 内存数据库)创建客户端。
- `advanced_options/`:演示 GORM 高级配置与连接池选项,如 `NamingStrategy``SkipDefaultTransaction``DisableForeignKeyConstraintWhenMigrating` 等。
## 运行示例
示例目录下每个文件可单独执行:
```bash
cd c:/Code/pkg/mysqlx
go run ./example/basic_dsn/basic_dsn.go
```
```bash
go run ./example/expanded_dsn/expanded_dsn.go
```
```bash
go run ./example/custom_dialector/custom_dialector.go
```
```bash
go run ./example/advanced_options/gorm_advanced_options.go
```
也可以运行整个 `example` 目录里的单个子目录或文件。
@@ -0,0 +1,29 @@
package main
import (
"fmt"
"log"
"time"
"code.yun.ink/pkg/mysqlx"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
func main() {
client, err := mysqlx.NewDB(
mysqlx.WithDSN("user:password@tcp(127.0.0.1:3306)/testdb?parseTime=true&loc=Local"),
mysqlx.WithLogger(logger.Default.LogMode(logger.Info)),
mysqlx.WithNamingStrategy(schema.NamingStrategy{SingularTable: true}),
mysqlx.WithDisableForeignKeyConstraintWhenMigrating(true),
mysqlx.WithSkipDefaultTransaction(true),
mysqlx.WithConnectionPool(5, 20, 15*time.Minute, time.Hour),
)
if err != nil {
log.Fatalf("failed to create mysqlx client: %v", err)
}
defer client.Close()
fmt.Println("MySQL client created with advanced GORM and connection pool options")
_ = client
}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"fmt"
"log"
"code.yun.ink/pkg/mysqlx"
"gorm.io/gorm/logger"
)
func main() {
client, err := mysqlx.NewDB(
mysqlx.WithDSN("user:password@tcp(127.0.0.1:3306)/testdb?parseTime=true&loc=Local"),
mysqlx.WithLogger(logger.Default.LogMode(logger.Info)),
)
if err != nil {
log.Fatalf("failed to create mysqlx client: %v", err)
}
defer client.Close()
fmt.Println("MySQL client created with raw DSN")
_ = client
}
@@ -0,0 +1,24 @@
package main
import (
"fmt"
"log"
"code.yun.ink/pkg/mysqlx"
"gorm.io/driver/sqlite"
"gorm.io/gorm/logger"
)
func main() {
client, err := mysqlx.NewDB(
mysqlx.WithDialector(sqlite.Open("file::memory:?cache=shared")),
mysqlx.WithLogger(logger.Default.LogMode(logger.Info)),
)
if err != nil {
log.Fatalf("failed to create mysqlx client with custom dialector: %v", err)
}
defer client.Close()
fmt.Println("MySQL SDK client created with custom Dialector")
_ = client
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"fmt"
"log"
"time"
"code.yun.ink/pkg/mysqlx"
"github.com/go-sql-driver/mysql"
"gorm.io/gorm/logger"
)
func main() {
cfg := mysql.NewConfig()
cfg.User = "testuser"
cfg.Passwd = "password"
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "testdb"
cfg.ParseTime = true
cfg.Loc = time.Local
cfg.Params = map[string]string{
"charset": "utf8mb4",
}
client, err := mysqlx.NewDB(
mysqlx.WithDSNConfig(cfg),
mysqlx.WithDSNTimeout(5*time.Second),
mysqlx.WithDSNReadTimeout(10*time.Second),
mysqlx.WithDSNWriteTimeout(10*time.Second),
mysqlx.WithDSNParams(map[string]string{"multiStatements": "true"}),
mysqlx.WithLogger(logger.Default.LogMode(logger.Warn)),
)
if err != nil {
log.Fatalf("failed to create mysqlx client: %v", err)
}
defer client.Close()
fmt.Println("MySQL client created with expanded DSN configuration")
_ = client
}
+19
View File
@@ -0,0 +1,19 @@
module code.yun.ink/pkg/mysqlx
go 1.26.3
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/go-sql-driver/mysql v1.8.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
golang.org/x/text v0.20.0 // indirect
)
+21
View File
@@ -0,0 +1,21 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+80
View File
@@ -0,0 +1,80 @@
package mysqlx
import (
drivermysql "gorm.io/driver/mysql"
"gorm.io/gorm"
)
// Client is the wrapper around a gorm DB instance.
type Client struct {
db *gorm.DB
}
// New creates a Client using functional options.
// It requires either a DSN or a custom gorm Dialector.
func NewDB(opts ...Option) (*Client, error) {
cfg := defaultConfig()
for _, opt := range opts {
opt(cfg)
}
if cfg.Dialector == nil {
dsn, err := buildDSN(cfg)
if err != nil {
return nil, err
}
cfg.Dialector = drivermysql.Open(dsn)
}
gormConfig := &gorm.Config{
Logger: cfg.Logger,
NamingStrategy: cfg.NamingStrategy,
DisableForeignKeyConstraintWhenMigrating: cfg.DisableForeignKeyConstraintWhenMigrating,
SkipDefaultTransaction: cfg.SkipDefaultTransaction,
DryRun: cfg.DryRun,
}
db, err := gorm.Open(cfg.Dialector, gormConfig)
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
if cfg.MaxIdleConns > 0 {
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
}
if cfg.MaxOpenConns > 0 {
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
}
if cfg.ConnMaxIdleTime > 0 {
sqlDB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
}
if cfg.ConnMaxLifetime > 0 {
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
}
return &Client{db: db}, nil
}
// DB returns the underlying gorm DB.
func (c *Client) DB() *gorm.DB {
return c.db
}
// Close closes the underlying sql.DB connection pool.
func (c *Client) Close() error {
sqlDB, err := c.db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
// AutoMigrate runs gorm AutoMigrate for the provided models.
func (c *Client) AutoMigrate(models ...interface{}) error {
return c.db.AutoMigrate(models...)
}
+62
View File
@@ -0,0 +1,62 @@
package mysqlx
import (
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
drivermysql "gorm.io/driver/mysql"
"gorm.io/gorm/logger"
)
type testModel struct {
ID uint `gorm:"primaryKey"`
Name string
}
func TestNewWithCustomDialector(t *testing.T) {
sqlDB, _, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
defer sqlDB.Close()
client, err := NewDB(
WithDialector(drivermysql.New(drivermysql.Config{Conn: sqlDB})),
WithLogger(logger.Default.LogMode(logger.Silent)),
WithConnectionPool(2, 5, 5*time.Minute, 10*time.Minute),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer client.Close()
if client.DB() == nil {
t.Fatal("expected non-nil DB")
}
}
func TestBuildDSNWithExpandedConfig(t *testing.T) {
cfg := defaultConfig()
WithDSNUser("testuser")(cfg)
WithDSNPassword("password")(cfg)
WithDSNNet("tcp")(cfg)
WithDSNAddr("127.0.0.1:3306")(cfg)
WithDSNDBName("testdb")(cfg)
WithDSNParams(map[string]string{"parseTime": "true", "loc": "Local"})(cfg)
WithDSNTLSConfig("false")(cfg)
dsn, err := buildDSN(cfg)
if err != nil {
t.Fatalf("buildDSN returned error: %v", err)
}
if dsn == "" {
t.Fatal("expected non-empty DSN")
}
}
func TestNewWithoutDSNOrDialector(t *testing.T) {
_, err := NewDB()
if err == nil {
t.Fatal("expected error when DSN and Dialector are missing")
}
}
+340
View File
@@ -0,0 +1,340 @@
package mysqlx
import (
"errors"
"time"
mysqldriver "github.com/go-sql-driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
// Option configures the SDK client.
type Option func(*config)
type config struct {
DSN string
DSNConfig *mysqldriver.Config
DSNUser string
DSNPassword string
DSNNet string
DSNAddr string
DSNDBName string
DSNParams map[string]string
DSNParseTime *bool
DSNTLSConfig string
DSNLoc *time.Location
DSNCollation string
DSNTimeout time.Duration
DSNReadTimeout time.Duration
DSNWriteTimeout time.Duration
Dialector gorm.Dialector
Logger logger.Interface
NamingStrategy schema.NamingStrategy
MaxIdleConns int
MaxOpenConns int
ConnMaxIdleTime time.Duration
ConnMaxLifetime time.Duration
DisableForeignKeyConstraintWhenMigrating bool
SkipDefaultTransaction bool
DryRun bool
}
type DSN struct {
}
func defaultConfig() *config {
return &config{
Logger: logger.Default,
NamingStrategy: schema.NamingStrategy{},
MaxIdleConns: 10,
MaxOpenConns: 100,
}
}
func buildDSN(cfg *config) (string, error) {
var mysqlConfig *mysqldriver.Config
if cfg.DSN != "" {
parsed, err := mysqldriver.ParseDSN(cfg.DSN)
if err != nil {
return "", err
}
mysqlConfig = parsed
} else if cfg.DSNConfig != nil {
mysqlConfig = cfg.DSNConfig.Clone()
} else {
mysqlConfig = mysqldriver.NewConfig()
}
applyMySQLConfigOverrides(mysqlConfig, cfg)
if mysqlConfig.User == "" && mysqlConfig.Passwd == "" && mysqlConfig.Addr == "" && mysqlConfig.DBName == "" && len(mysqlConfig.Params) == 0 {
if cfg.DSN == "" {
return "", errors.New("mysqlx: DSN or expanded DSN configuration must be provided")
}
}
return mysqlConfig.FormatDSN(), nil
}
func applyMySQLConfigOverrides(mysqlConfig *mysqldriver.Config, cfg *config) {
if cfg.DSNConfig != nil {
mergeMySQLConfig(mysqlConfig, cfg.DSNConfig)
}
if cfg.DSNUser != "" {
mysqlConfig.User = cfg.DSNUser
}
if cfg.DSNPassword != "" {
mysqlConfig.Passwd = cfg.DSNPassword
}
if cfg.DSNNet != "" {
mysqlConfig.Net = cfg.DSNNet
}
if cfg.DSNAddr != "" {
mysqlConfig.Addr = cfg.DSNAddr
}
if cfg.DSNDBName != "" {
mysqlConfig.DBName = cfg.DSNDBName
}
if cfg.DSNParams != nil {
if mysqlConfig.Params == nil {
mysqlConfig.Params = map[string]string{}
}
for key, value := range cfg.DSNParams {
mysqlConfig.Params[key] = value
}
}
if cfg.DSNParseTime != nil {
mysqlConfig.ParseTime = *cfg.DSNParseTime
}
if cfg.DSNTLSConfig != "" {
mysqlConfig.TLSConfig = cfg.DSNTLSConfig
}
if cfg.DSNLoc != nil {
mysqlConfig.Loc = cfg.DSNLoc
}
if cfg.DSNCollation != "" {
mysqlConfig.Collation = cfg.DSNCollation
}
if cfg.DSNTimeout > 0 {
mysqlConfig.Timeout = cfg.DSNTimeout
}
if cfg.DSNReadTimeout > 0 {
mysqlConfig.ReadTimeout = cfg.DSNReadTimeout
}
if cfg.DSNWriteTimeout > 0 {
mysqlConfig.WriteTimeout = cfg.DSNWriteTimeout
}
}
func mergeMySQLConfig(base, override *mysqldriver.Config) {
if override.User != "" {
base.User = override.User
}
if override.Passwd != "" {
base.Passwd = override.Passwd
}
if override.Net != "" {
base.Net = override.Net
}
if override.Addr != "" {
base.Addr = override.Addr
}
if override.DBName != "" {
base.DBName = override.DBName
}
if override.Params != nil {
if base.Params == nil {
base.Params = map[string]string{}
}
for key, value := range override.Params {
base.Params[key] = value
}
}
if override.Collation != "" {
base.Collation = override.Collation
}
if override.Loc != nil {
base.Loc = override.Loc
}
if override.TLSConfig != "" {
base.TLSConfig = override.TLSConfig
}
if override.Timeout > 0 {
base.Timeout = override.Timeout
}
if override.ReadTimeout > 0 {
base.ReadTimeout = override.ReadTimeout
}
if override.WriteTimeout > 0 {
base.WriteTimeout = override.WriteTimeout
}
if override.ParseTime {
base.ParseTime = override.ParseTime
}
}
// WithDSN sets the MySQL DSN used to open the database.
func WithDSN(dsn string) Option {
return func(cfg *config) {
cfg.DSN = dsn
}
}
// WithDSNConfig sets the underlying mysql.Config used to generate the DSN.
func WithDSNConfig(mysqlConfig *mysqldriver.Config) Option {
return func(cfg *config) {
cfg.DSNConfig = mysqlConfig
}
}
// WithDSNUser sets the MySQL username.
func WithDSNUser(user string) Option {
return func(cfg *config) {
cfg.DSNUser = user
}
}
// WithDSNPassword sets the MySQL password.
func WithDSNPassword(password string) Option {
return func(cfg *config) {
cfg.DSNPassword = password
}
}
// WithDSNNet sets the network type for the DSN.
func WithDSNNet(net string) Option {
return func(cfg *config) {
cfg.DSNNet = net
}
}
// WithDSNAddr sets the database address for the DSN.
func WithDSNAddr(addr string) Option {
return func(cfg *config) {
cfg.DSNAddr = addr
}
}
// WithDSNDBName sets the database name for the DSN.
func WithDSNDBName(dbName string) Option {
return func(cfg *config) {
cfg.DSNDBName = dbName
}
}
// WithDSNParams sets additional DSN query parameters.
func WithDSNParams(params map[string]string) Option {
return func(cfg *config) {
if cfg.DSNParams == nil {
cfg.DSNParams = map[string]string{}
}
for key, value := range params {
cfg.DSNParams[key] = value
}
}
}
// WithDSNParseTime configures parseTime for the DSN.
func WithDSNParseTime(parseTime bool) Option {
return func(cfg *config) {
cfg.DSNParseTime = &parseTime
}
}
// WithDSNTLSConfig sets the TLS configuration name for the DSN.
func WithDSNTLSConfig(tlsConfig string) Option {
return func(cfg *config) {
cfg.DSNTLSConfig = tlsConfig
}
}
// WithDSNLocation sets the time location for DSN parsing.
func WithDSNLocation(loc *time.Location) Option {
return func(cfg *config) {
cfg.DSNLoc = loc
}
}
// WithDSNCollation sets the connection collation for the DSN.
func WithDSNCollation(collation string) Option {
return func(cfg *config) {
cfg.DSNCollation = collation
}
}
// WithDSNTimeout sets the dial timeout for the DSN.
func WithDSNTimeout(timeout time.Duration) Option {
return func(cfg *config) {
cfg.DSNTimeout = timeout
}
}
// WithDSNReadTimeout sets the read timeout for the DSN.
func WithDSNReadTimeout(timeout time.Duration) Option {
return func(cfg *config) {
cfg.DSNReadTimeout = timeout
}
}
// WithDSNWriteTimeout sets the write timeout for the DSN.
func WithDSNWriteTimeout(timeout time.Duration) Option {
return func(cfg *config) {
cfg.DSNWriteTimeout = timeout
}
}
// WithDialector sets a custom gorm Dialector.
func WithDialector(dialector gorm.Dialector) Option {
return func(cfg *config) {
cfg.Dialector = dialector
}
}
// WithLogger sets the gorm logger.
func WithLogger(logger logger.Interface) Option {
return func(cfg *config) {
cfg.Logger = logger
}
}
// WithNamingStrategy sets the naming strategy for gorm models.
func WithNamingStrategy(strategy schema.NamingStrategy) Option {
return func(cfg *config) {
cfg.NamingStrategy = strategy
}
}
// WithConnectionPool configures database connection pooling.
func WithConnectionPool(maxIdleConns, maxOpenConns int, maxIdleTime, maxLifetime time.Duration) Option {
return func(cfg *config) {
cfg.MaxIdleConns = maxIdleConns
cfg.MaxOpenConns = maxOpenConns
cfg.ConnMaxIdleTime = maxIdleTime
cfg.ConnMaxLifetime = maxLifetime
}
}
// WithDisableForeignKeyConstraintWhenMigrating toggles foreign key constraint creation during migrations.
func WithDisableForeignKeyConstraintWhenMigrating(disable bool) Option {
return func(cfg *config) {
cfg.DisableForeignKeyConstraintWhenMigrating = disable
}
}
// WithSkipDefaultTransaction toggles default transactions in gorm.
func WithSkipDefaultTransaction(skip bool) Option {
return func(cfg *config) {
cfg.SkipDefaultTransaction = skip
}
}
// WithDryRun toggles gorm dry run mode.
func WithDryRun(dryRun bool) Option {
return func(cfg *config) {
cfg.DryRun = dryRun
}
}