尝试用golang 1.18泛型实现orm

这几天golang社区对泛型的讨论非常多的,一片热火朝天的景象。对我们广大gopher来说总归是好事。

泛型很有可能会颠覆我们之前的很多设计,带着这种疑问和冲动,我准备尝试用golang泛型实现几个orm的常见功能。

本文并没完全实现通用的orm,只是探讨其实现的一种方式提供各位读者做借鉴。

创建Table

虽然golang有了泛型,但是目前在标准库sql底层还没有改造,目前还有很多地方需要用到reflect

func CreateTable[T any](db *sql.DB) { var a T t := reflect.TypeOf(a) tableName := strings.ToLower(t.Name()) var desc string for i := 0; i < t.NumField(); i++ { columnsName := strings.ToLower(t.Field(i).Name) var columnType string switch t.Field(i).Type.Kind() { case reflect.Int: columnType = "integer" case reflect.String: columnType = "text" } desc += columnsName + " " + columnType if i < t.NumField()-1 { desc += "," } } sqlStmt := fmt.Sprintf(`create table if not exists %s (%s);`, tableName, desc) _, err := db.Exec(sqlStmt) if err != nil { log.Printf("%q: %s\n", err, sqlStmt) return } }

调用方式

type Person struct { ID int Name string Age int } type Student struct { ID int Name string No string } var db sql.DB //init db //... CreateTable[Person](db) CreateTable[Student](db)

这个部分跟传统的orm使用上没有太大区别,没办法不使用反射的情况下,泛型的方式可能变得有点繁琐。

写入数据

func Create[T any](db *sql.DB, a T) { //没有办法这边还是得使用反射 t := reflect.TypeOf(a) tableName := strings.ToLower(t.Name()) var columns []string var spacehold []string for i := 0; i < t.NumField(); i++ { columns = append(columns, strings.ToLower(t.Field(i).Name)) spacehold = append(spacehold, "?") } tx, err := db.Begin() if err != nil { log.Fatal(err) } stmt, err := tx.Prepare( fmt.Sprintf("insert into %s(%s) values(%s)", tableName, strings.Join(columns, ","), strings.Join(spacehold, ","))) if err != nil { log.Fatal(err) } defer stmt.Close() v := reflect.ValueOf(a) var values []any for i := 0; i < t.NumField(); i++ { if v.FieldByName(t.Field(i).Name).CanInt() { values = append(values, v.FieldByName(t.Field(i).Name).Int()) } else { values = append(values, v.FieldByName(t.Field(i).Name).String()) } } _, err = stmt.Exec(values...) if err != nil { panic(err) } tx.Commit() }

调用方式

var p1 = Person{ ID: 1, Name: "wida", } Create[Person](db, p1) var s1 = Student{ ID: 1, Name: "wida", No: "1111", } Create[Person](db, p1) Create[Student](db, s1)

和创建table类似,写入数据好像比没有之前的orm有优势。

读取数据

读取数据是非常高频的操作,所以我们稍作封装。

type Client struct { db *sql.DB } type Query[T any] struct { client *Client } func NewQuery[T any](c *Client) *Query[T] { return &Query[T]{ client: c, } } //反射到struct func ToStruct[T any](rows *sql.Rows, to T) error { v := reflect.ValueOf(to) if v.Elem().Type().Kind() != reflect.Struct { return errors.New("Expect a struct") } scanDest := []any{} columnNames, _ := rows.Columns() addrByColumnName := map[string]any{} for i := 0; i < v.Elem().NumField(); i++ { oneValue := v.Elem().Field(i) columnName := strings.ToLower(v.Elem().Type().Field(i).Name) addrByColumnName[columnName] = oneValue.Addr().Interface() } for _, columnName := range columnNames { scanDest = append(scanDest, addrByColumnName[columnName]) } return rows.Scan(scanDest...) } func (q *Query[T]) FetchAll(ctx context.Context) ([]T, error) { var items []T var a T t := reflect.TypeOf(a) tableName := strings.ToLower(t.Name()) rows, err := q.client.db.Query("SELECT * FROM " + tableName) if err != nil { return nil, err } for rows.Next() { var c T ToStruct(rows, &c) items = append(items, c) } return items, nil }

调用方式

var client = &Client{ db: db, } { query := NewQuery[Person](client) all, err := query.FetchAll(context.Background()) if err != nil { log.Fatal(err) } for _, person := range all { log.Println(person) } } { query := NewQuery[Student](client) all, err := query.FetchAll(context.Background()) if err != nil { log.Fatal(err) } for _, person := range all { log.Println(person) } }

稍微比原先的orm方式有了多一点想象空间,比如 在[T any]做更明确的约束,比如要求实现Filter定制方法。

总结

鉴于本人能力还认证有限,目前还没有发现泛型对orm剧烈的改进和突破的可能。未来如果go对底层sql做出改动,或者实现诸如Rust那种Enum方式,可能会带来更多的惊喜。