动手写ORM框架 - GeeORM第二天 对象表结构映射
源代码/数据集已上传到
Github - 7days-golang
本文是7天用Go从零实现ORM框架GeeORM 的第二篇。
使用 dialect 隔离不同数据库之间的差异,便于扩展。
使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。
数据库表的创建(create)、删除(drop)。代码约150行
1 Dialect SQL 语句中的类型和 Go 语言中的类型是不同的,例如Go 语言中的 int
、int8
、int16
等类型均对应 SQLite 中的 integer
类型。因此实现 ORM 映射的第一步,需要思考如何将 Go 语言的类型映射为数据库中的类型。
同时,不同数据库支持的数据类型也是有差异的,即使功能相同,在 SQL 语句的表达上也可能有差异。ORM 框架往往需要兼容多种数据库,因此我们需要将差异的这一部分提取出来,每一种数据库分别实现,实现最大程度的复用和解耦。这部分代码称之为 dialect
。
在根目录下新建文件夹 dialect,并在 dialect 文件夹下新建文件 dialect.go
,抽象出各个数据库差异的部分。
day2-reflect-schema/dialect/dialect.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package dialectimport "reflect" var dialectsMap = map [string ]Dialect{}type Dialect interface { DataTypeOf(typ reflect.Value) string TableExistSQL(tableName string ) (string , []interface {}) } func RegisterDialect (name string , dialect Dialect) { dialectsMap[name] = dialect } func GetDialect (name string ) (dialect Dialect, ok bool ) { dialect, ok = dialectsMap[name] return }
Dialect
接口包含 2 个方法:
DataTypeOf
用于将 Go 语言的类型转换为该数据库的数据类型。
TableExistSQL
返回某个表是否存在的 SQL 语句,参数是表名(table)。
当然,不同数据库之间的差异远远不止这两个地方,随着 ORM 框架功能的增多,dialect 的实现也会逐渐丰富起来,同时框架的其他部分不会受到影响。
同时,声明了 RegisterDialect
和 GetDialect
两个方法用于注册和获取 dialect 实例。如果新增加对某个数据库的支持,那么调用 RegisterDialect
即可注册到全局。
接下来,在dialect
目录下新建文件 sqlite3.go
增加对 SQLite 的支持。
day2-reflect-schema/dialect/sqlite3.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package dialectimport ( "fmt" "reflect" "time" ) type sqlite3 struct {}var _ Dialect = (*sqlite3)(nil )func init () { RegisterDialect("sqlite3" , &sqlite3{}) } func (s *sqlite3) DataTypeOf (typ reflect.Value) string { switch typ.Kind() { case reflect.Bool: return "bool" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: return "integer" case reflect.Int64, reflect.Uint64: return "bigint" case reflect.Float32, reflect.Float64: return "real" case reflect.String: return "text" case reflect.Array, reflect.Slice: return "blob" case reflect.Struct: if _, ok := typ.Interface().(time.Time); ok { return "datetime" } } panic (fmt.Sprintf("invalid sql type %s (%s)" , typ.Type().Name(), typ.Kind())) } func (s *sqlite3) TableExistSQL (tableName string ) (string , []interface {}) { args := []interface {}{tableName} return "SELECT name FROM sqlite_master WHERE type='table' and name = ?" , args }
sqlite3.go
的实现虽然比较繁琐,但是整体逻辑还是非常清晰的。DataTypeOf
将 Go 语言的类型映射为 SQLite 的数据类型。TableExistSQL
返回了在 SQLite 中判断表 tableName
是否存在的 SQL 语句。
实现了 init()
函数,包在第一次加载时,会将 sqlite3 的 dialect 自动注册到全局。
2 Schema Dialect 实现了一些特定的 SQL 语句的转换,接下来我们将要实现 ORM 框架中最为核心的转换——对象(object)和表(table)的转换。给定一个任意的对象,转换为关系型数据库中的表结构。
在数据库中创建一张表需要哪些要素呢?
表名(table name) —— 结构体名(struct name)
字段名和字段类型 —— 成员变量和类型。
额外的约束条件(例如非空、主键等) —— 成员变量的Tag(Go 语言通过 Tag 实现,Java、Python 等语言通过注解实现)
举一个实际的例子:
1 2 3 4 type User struct { Name string `geeorm:"PRIMARY KEY"` Age int }
期望对应的 schema 语句:
1 CREATE TABLE `User ` (`Name` text PRIMARY KEY, `Age` integer );
我们将这部分代码的实现放置在一个子包 schema/schema.go
中。
day2-reflect-schema/schema/schema.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package schemaimport ( "geeorm/dialect" "go/ast" "reflect" ) type Field struct { Name string Type string Tag string } type Schema struct { Model interface {} Name string Fields []*Field FieldNames []string fieldMap map [string ]*Field } func (schema *Schema) GetField (name string ) *Field { return schema.fieldMap[name] }
Field 包含 3 个成员变量,字段名 Name、类型 Type、和约束条件 Tag
Schema 主要包含被映射的对象 Model、表名 Name 和字段 Fields。
FieldNames 包含所有的字段名(列名),fieldMap 记录字段名和 Field 的映射关系,方便之后直接使用,无需遍历 Fields。
接下来实现 Parse 函数,将任意的对象解析为 Schema 实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func Parse (dest interface {}, d dialect.Dialect) *Schema { modelType := reflect.Indirect(reflect.ValueOf(dest)).Type() schema := &Schema{ Model: dest, Name: modelType.Name(), fieldMap: make (map [string ]*Field), } for i := 0 ; i < modelType.NumField(); i++ { p := modelType.Field(i) if !p.Anonymous && ast.IsExported(p.Name) { field := &Field{ Name: p.Name, Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))), } if v, ok := p.Tag.Lookup("geeorm" ); ok { field.Tag = v } schema.Fields = append (schema.Fields, field) schema.FieldNames = append (schema.FieldNames, p.Name) schema.fieldMap[p.Name] = field } } return schema }
TypeOf()
和 ValueOf()
是 reflect 包最为基本也是最重要的 2 个方法,分别用来返回入参的类型和值。因为设计的入参是一个对象的指针,因此需要 reflect.Indirect()
获取指针指向的实例。
modelType.Name()
获取到结构体的名称作为表名。
NumField()
获取实例的字段的个数,然后通过下标获取到特定字段 p := modelType.Field(i)
。
p.Name
即字段名,p.Type
即字段类型,通过 (Dialect).DataTypeOf()
转换为数据库的字段类型,p.Tag
即额外的约束条件。
写一个测试用例来验证 Parse 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type User struct { Name string `geeorm:"PRIMARY KEY"` Age int } var TestDial, _ = dialect.GetDialect("sqlite3" )func TestParse (t *testing.T) { schema := Parse(&User{}, TestDial) if schema.Name != "User" || len (schema.Fields) != 2 { t.Fatal("failed to parse User struct" ) } if schema.GetField("Name" ).Tag != "PRIMARY KEY" { t.Fatal("failed to parse primary key" ) } }
3 Session Session 的核心功能是与数据库进行交互。因此,我们将数据库表的增/删操作实现在子包 session 中。在此之前,Session 的结构需要做一些调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Session struct { db *sql.DB dialect dialect.Dialect refTable *schema.Schema sql strings.Builder sqlVars []interface {} } func New (db *sql.DB, dialect dialect.Dialect) *Session { return &Session{ db: db, dialect: dialect, } }
Session
成员变量新增 dialect 和 refTable
构造函数 New
的参数改为 2 个,db 和 dialect。
在文件夹 session
下新建 table.go
用于放置操作数据库表相关的代码。
day2-reflect-schema/session/table.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (s *Session) Model (value interface {}) *Session { if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) { s.refTable = schema.Parse(value, s.dialect) } return s } func (s *Session) RefTable () *schema .Schema { if s.refTable == nil { log.Error("Model is not set" ) } return s.refTable }
Model()
方法用于给 refTable 赋值。解析操作是比较耗时的,因此将解析的结果保存在成员变量 refTable 中,即使 Model()
被调用多次,如果传入的结构体名称不发生变化,则不会更新 refTable 的值。
RefTable()
方法返回 refTable 的值,如果 refTable 未被赋值,则打印错误日志。
接下来实现数据库表的创建、删除和判断是否存在的功能。三个方法的实现逻辑是相似的,利用 RefTable()
返回的数据库表和字段的信息,拼接出 SQL 语句,调用原生 SQL 接口执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (s *Session) CreateTable () error { table := s.RefTable() var columns []string for _, field := range table.Fields { columns = append (columns, fmt.Sprintf("%s %s %s" , field.Name, field.Type, field.Tag)) } desc := strings.Join(columns, "," ) _, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);" , table.Name, desc)).Exec() return err } func (s *Session) DropTable () error { _, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s" , s.RefTable().Name)).Exec() return err } func (s *Session) HasTable () bool { sql, values := s.dialect.TableExistSQL(s.RefTable().Name) row := s.Raw(sql, values...).QueryRow() var tmp string _ = row.Scan(&tmp) return tmp == s.RefTable().Name }
在 table_test.go
中实现对应的测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 type User struct { Name string `geeorm:"PRIMARY KEY"` Age int } func TestSession_CreateTable (t *testing.T) { s := NewSession().Model(&User{}) _ = s.DropTable() _ = s.CreateTable() if !s.HasTable() { t.Fatal("Failed to create table User" ) } }
4 Engine 因为 Session 构造函数增加了对 dialect 的依赖,Engine 需要作一些细微的调整。
day2-reflect-schema/geeorm.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type Engine struct { db *sql.DB dialect dialect.Dialect } func NewEngine (driver, source string ) (e *Engine, err error) { db, err := sql.Open(driver, source) if err != nil { log.Error(err) return } if err = db.Ping(); err != nil { log.Error(err) return } dial, ok := dialect.GetDialect(driver) if !ok { log.Errorf("dialect %s Not Found" , driver) return } e = &Engine{db: db, dialect: dial} log.Info("Connect database success" ) return } func (engine *Engine) NewSession () *session .Session { return session.New(engine.db, engine.dialect) }
NewEngine
创建 Engine 实例时,获取 driver 对应的 dialect。
NewSession
创建 Session 实例时,传递 dialect 给构造函数 New。
至此,第二天的内容已经完成了,总结一下今天的成果:
1)为适配不同的数据库,映射数据类型和特定的 SQL 语句,创建 Dialect 层屏蔽数据库差异。
2)设计 Schema,利用反射(reflect)完成结构体和数据库表结构的映射,包括表名、字段名、字段类型、字段 tag 等。
3)构造创建(create)、删除(drop)、存在性(table exists) 的 SQL 语句完成数据库表的基本操作。
附 推荐阅读
last updated at 2023-11-15