2. 对象表结构映射

Kesa...大约 4 分钟golang

day2-ormopen in new window

1. Dialect

不同的数据库对于SQL语句支持可能不同,ORM框架需要兼容多种数据库,将各数据不同的地方提取出来单独实现,这种方式被称为Dialect

var dialectsMap = map[string]Dialect{}

type Dialect interface {
	DataTypeOf(typ reflect.Value) string
	TableExistSQL(tableName string) (string, []any)
}

func RegisterDialect(name string, dialect Dialect) {
	dialectsMap[name] = dialect
}

func GetDialect(name string) (Dialect, bool) {
	dialect, ok := dialectsMap[name]
	return dialect, ok
}

Dialect接口定义了两个方法:

  1. DataTpyeOf:将 Golang 的数据类型转换为数据库的数据类型
  2. TableExistSQL:返回某个表是否存在的 SQL 语句

全局变量dialectsMap存储不同的数据库对应的Dialect

1.1 SQLite3

package dialect

import (
	"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, []any) {
	args := []any{tableName}
	return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
}
  • init:函数在包第一次加载时,会将sqlite3dialect进行注册

2. Schema

ORM 框架最为核心的功能就是对象(Object)和数据库表(Table)的转换,表结构和结构体可以形成映射关系:

  1. 表名(Table Name):结构体名(Struct Name)
  2. 字段名/类型:结构体字段名/类型
  3. 约束条件:结构体字段的标签(tag)

例如:

type User struct {
    Name string `geeorm:"PRIMARY KEY"`
    Age  int
}

对应:

CREATE TABLE `User` (`Name` text PRIMARY KEY, `Age` integer);
package schema

// Field represents a column of database
type Field struct {
	Name string
	Type string
	Tag  string
}

// Schema represents a table of database
type Schema struct {
	Model      any
	Name       string
	Fields     []*Field
	FieldNames []string
	fieldMap   map[string]*Field
}

func (s *Schema) GetField(name string) *Field {
	return s.fieldMap[name]
}

  • Field:表示数据表字段
    1. Name:字段名
    2. Type:数据类型
    3. Tag:约束条件
  • Schema:表示数据表
    1. Model:映射的对象
    2. Name:表名
    3. Fields:表字段列表
    4. FieldNames:表字段名列表
    5. fieldMap:字段名/字段映射表,用于通过字段名快速获取字段
func Parse(dst any, d dialect.Dialect) *Schema {
	modelType := reflect.Indirect(reflect.ValueOf(dst)).Type()
	schema := &Schema{
		Model:    dst,
		Name:     modelType.Name(),
		fieldMap: make(map[string]*Field),
	}

	for i := 0; i < modelType.NumField(); i++ {
		f := modelType.Field(i)
		if !f.Anonymous && ast.IsExported(f.Name) {
			field := &Field{
				Name: f.Name,
				Type: d.DataTypeOf(reflect.Indirect(reflect.New(f.Type))),
			}
			if v, ok := f.Tag.Lookup("geeorm"); ok {
				field.Tag = v
			}
			schema.Fields = append(schema.Fields, field)
			schema.FieldNames = append(schema.FieldNames, f.Name)
			schema.fieldMap[f.Name] = field
		}
	}

	return schema
}
  1. 获取对象的数据类型,设计的入参类型为指针,使用reflect.Indirect获取指针指向的对象

  2. 通过modleType.Name获取结构体名称

  3. 遍历结构体字段,若字段不是匿名字段并且是导出的:

    1. 获取字段名
    2. 将字段类型转换成数据库数据类型
    3. 获取字段标签
    4. 构建Field实例
    5. 添加至Schema.Fields,Schema.FieldNames,Schema.fileMap
  4. 返回Schema实例

2.1 单元测试


type User struct {
	Name string `geeorm:"PRIMARY KEY"`
	Age  int
}

var testDialect, _ = dialect.GetDialect("sqlite3")

func TestParse(t *testing.T) {
	schema := Parse(&User{}, testDialect)
	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数据结构。

type Session struct {
	db       *sql.DB
	dialect  dialect.Dialect
	refTable *schema.Schema
	sql      strings.Builder
	sqlVars  []any
}

func New(db *sql.DB, dialect dialect.Dialect) *Session {
	return &Session{
		db:      db,
		dialect: dialect,
	}
}

新建文件session/table.go,用于放置数据库表相关的代码

package session

import (
	"geeorm/log"
	"geeorm/schema"
	"reflect"
)

func (s *Session) Model(val any) *Session {
	// nil or different model, update refTable
	if s.refTable == nil || reflect.TypeOf(val) != reflect.TypeOf(s.refTable.Model) {
		s.refTable = schema.Parse(val, s.dialect)
	}
	return s
}

func (s *Session) RefTable() *schema.Schema {
	if s.refTable == nil {
		log.Error("Model is not set")
	}
	return s.refTable
}
  • Model用于给Session.refTable赋值,因为解析操作开销较大,所以结构体不发生变化就不会重复进行解析
  • RefTable获取Session.refTable,若为空则打印日志

实现数据表的创建、删除和判断是否存在:

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, vals := s.dialect.TableExistSQL(s.RefTable().Name)
	row := s.Raw(sql, vals...).QueryRow()
	var tmp string
	_ = row.Scan(&tmp)
	return tmp == s.RefTable().Name
}

3.1 单元测试

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

修改Engine的数据结构。

type Engine struct {
	db      *sql.DB
	dialect dialect.Dialect
}

func NewEngine(driver, source string) (*Engine, error) {
	db, err := sql.Open(driver, source)
	if err != nil {
		log.Error(err)
		return nil, err
	}

	// Send ping to make sure the database connection is alive
	if err = db.Ping(); err != nil {
		log.Error(err)
		return nil, err
	}

	// make sure the specific dialect exists
	d, ok := dialect.GetDialect(driver)
	if !ok {
		log.Errorf("dialect %s Not Found", driver)
		return nil, DialectNotFoundErr
	}

	e := &Engine{db: db, dialect: d}
	log.Info("Connect database success")

	return e, nil
}

func (engine *Engine) NewSession() *session.Session {
	return session.New(engine.db, engine.dialect)
}

Reference

  1. https://geektutu.com/post/geeorm-day2.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2