diff --git a/README.md b/README.md index 1085a31..0afd030 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ type Config struct { insecureSkipVerify=true ``` + ## 5.2 优雅的Debug 通过开启``debug``配置和命令行的``export EGO_DEBUG=true``,我们就可以在测试环境里看到请求里的配置名、地址、耗时、请求数据、响应数据 ![image](./docs/images/ego_debug.png) @@ -125,6 +126,7 @@ func testDB() error { return err } ``` + ## 6 GORM的日志 任何gorm的请求都会记录gorm的错误access日志,如果需要对gorm的日志做定制化处理,可参考以下使用方式。 diff --git a/examples/sqlite/main.go b/examples/sqlite/main.go new file mode 100644 index 0000000..3728e0b --- /dev/null +++ b/examples/sqlite/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + + "github.com/ego-component/egorm" + "github.com/gotomicro/ego" + "github.com/gotomicro/ego/core/elog" +) + +// 运行方式: +// export EGO_DEBUG=true && go run main.go --config=config.toml +type KV struct { + ID int `gorm:"primaryKey" json:"id"` + K string `gorm:"uniqueIndex;not null" json:"k"` + V string `gorm:"not null" json:"v"` +} + +func (KV) TableName() string { return "kv" } + +var dbs []*egorm.Component + +func main() { + if err := ego.New().Invoker( + openDB, + run, + ).Run(); err != nil { + elog.Error("startup", elog.Any("err", err)) + } +} + +func openDB() error { + dbs = []*egorm.Component{ + egorm.Load("sqlite.test").Build(), + egorm.Load("sqlite.share.memory").Build(), + egorm.Load("sqlite.nonshared.memory").Build(), + } + for _, db := range dbs { + if err := db.AutoMigrate(&KV{}); err != nil { + return err + } + } + return nil +} + +func run() error { + ctx := context.Background() + for _, db := range dbs { + _ = db.WithContext(ctx).Create(&KV{K: "hello", V: "world"}).Error + var out KV + err := db.WithContext(ctx).Where("k = ?", "hello").First(&out).Error + elog.Info("kv", elog.String("k", out.K), elog.String("v", out.V), elog.FieldErr(err)) + } + return nil +} diff --git a/internal/dsn/sqlite.go b/internal/dsn/sqlite.go new file mode 100644 index 0000000..f8a598f --- /dev/null +++ b/internal/dsn/sqlite.go @@ -0,0 +1,91 @@ +package dsn + +import ( + "net/url" + "path/filepath" + "strings" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/schema" + + "github.com/ego-component/egorm/manager" +) + +var ( + _ manager.DSNParser = (*SqliteDSNParser)(nil) +) + +type SqliteDSNParser struct { +} + +func init() { + manager.Register(&SqliteDSNParser{}) +} + +func (p *SqliteDSNParser) Scheme() string { + return "sqlite" +} + +func (p *SqliteDSNParser) NamingStrategy() schema.Namer { + return nil +} + +func (p *SqliteDSNParser) GetDialector(dsn string) gorm.Dialector { + return sqlite.Open(dsn) +} + +// ParseDSN supports typical gorm sqlite DSN strings: +// - file based: "test.db", "/abs/path/to/test.db", "file:test.db?cache=shared&_fk=1" +// - memory: ":memory:", "file::memory:?cache=shared" +func (p *SqliteDSNParser) ParseDSN(dsn string) (cfg *manager.DSN, err error) { + cfg = new(manager.DSN) + cfg.Params = map[string]string{} + + // Normalize for parsing query parameters + raw := dsn + if strings.HasPrefix(raw, "file:") { + if idx := strings.IndexByte(raw, '?'); idx >= 0 { + query := raw[idx+1:] + for _, kv := range strings.Split(query, "&") { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + continue + } + val, decodeErr := url.QueryUnescape(parts[1]) + if decodeErr != nil { + continue + } + cfg.Params[parts[0]] = val + } + } + } + + // Determine DBName for logging/metrics only + switch { + // memory mode + case dsn == ":memory:": + case strings.Contains(dsn, "::memory:"): + cfg.DBName = "memory" + default: + trimmed := dsn + // strip "file:" prefix for name extraction + if strings.HasPrefix(trimmed, "file:") { + if idx := strings.IndexByte(trimmed, '?'); idx >= 0 { + trimmed = trimmed[:idx] + } + trimmed = strings.TrimPrefix(trimmed, "file:") + } else { + // if path like "dir/db.sqlite" keep base + if idx := strings.IndexByte(trimmed, '?'); idx >= 0 { + trimmed = trimmed[:idx] + } + } + base := filepath.Base(trimmed) + if base == "" || base == "." || base == "/" { + base = "sqlite" + } + cfg.DBName = base + } + return +} diff --git a/internal/dsn/sqlite_test.go b/internal/dsn/sqlite_test.go new file mode 100644 index 0000000..1b8c80c --- /dev/null +++ b/internal/dsn/sqlite_test.go @@ -0,0 +1,90 @@ +package dsn + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSqliteDSNParser_ParseDSN(t *testing.T) { + tests := []struct { + name string + dsn string + expected *struct { + DBName string + Addr string + Params map[string]string + } + }{ + { + name: "memory database", + dsn: ":memory:", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "memory", + Addr: "", + Params: map[string]string{}, + }, + }, + { + name: "file memory with cache", + dsn: "file::memory:?cache=shared", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "memory", + Addr: "", + Params: map[string]string{ + "cache": "shared", + }, + }, + }, + { + name: "file based database", + dsn: "test.db", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "test.db", + Addr: "", + Params: map[string]string{}, + }, + }, + { + name: "file URI with parameters", + dsn: "file:./data/app.db?cache=shared&_journal_mode=WAL&_busy_timeout=5000&_fk=1", + expected: &struct { + DBName string + Addr string + Params map[string]string + }{ + DBName: "app.db", + Addr: "", + Params: map[string]string{ + "cache": "shared", + "_journal_mode": "WAL", + "_busy_timeout": "5000", + "_fk": "1", + }, + }, + }, + } + + dsnParser := SqliteDSNParser{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := dsnParser.ParseDSN(tt.dsn) + assert.NoError(t, err) + assert.Equal(t, tt.expected.DBName, cfg.DBName) + assert.Equal(t, tt.expected.Addr, cfg.Addr) + assert.Equal(t, tt.expected.Params, cfg.Params) + }) + } +}