title: ORM
ORM
ORM(Object Relational Map)对象关系映射,一般指持久化数据(数据库?)和实体对象的映射。
在MVC(Model View Controller)架构中就是个简洁的Model
自动生成SQL语句
ORM框架
O(object)对应程序中的实体对象,它可以是一个map类型的数据,也可以是struct类型的数据(如果是struct类型,则需要使用反射(reflect)来获取字段名)。
R(relation)对应数据库,因为关系型数据库利用实体间的关系连接数据。
M(Map)我们此次任务需要实现的就是这个映射(map),很多语言已经有了orm框架
实现ORM的思路
建立映射
ORM一切的基础在OR映射上,CRUD操作也不例外。任务中规定的所有增删改查方法都需要传入一个结构体或map(实例),这个实例就对应着要操作的数据库字段。而映射规则就是:
(1)一个结构体或map对应一张表。类名是单数,且首字母大写;表名是复数,且全部是小写。比如,表books
对应类Book
。
(2)如果名字是不规则复数,则类名依照英语习惯命名,比如,表(我暂时就不写这么复杂的了mice
对应类Mouse
,表people
对应类Person
。
(3)如果名字包含多个单词,那么类名使用首字母全部大写的骆驼拼写法,而表名使用下划线分隔的小写单词。比如,表book_clubs
对应类BookClub
,表line_items
对应类LineItem
。
(4)每个表都必须有一个主键字段,通常是叫做id
的整数字段。外键字段名约定为单数的表名 + 下划线 + id,比如item_id
表示该字段对应items
表的id
字段。
(5)结构体或map的属性和字段名称一样
要利用反射获取结构体的变量名以及tag
连接数据库
连接到我的mysql数据库,返回一个db实例,这部操作放在所有增删改查的第一步。
增(这不算增)
CreateTable
CreateTable(Myobject interface{})(bool, error)
该方法会根据传入的实例创建一个表,表名根据映射规则与实例名对应,该方法会根据实例的内部变量创建一个空表,因此,内部变量的值可为空,字典对应的映射可为空。
如果表存在,返回一个bool值,如果这个表的字段与要创建的表不一致,返回一个错误。
func (b *orm )CreateTable(Myobject interface{})(bool, error) {
db := b.db
TypeRef := reflect.TypeOf(Myobject).Elem()
s := TypeRef.Name()
//获取表名
TableName := strings.ToUpper(s[:0]) + s[1:] + "s"
//查询是否有这个表
SqlQuery1 := "SELECT information_schema.SCHEMATA.SCHEMA_NAME FROM information_schema.SCHEMATA where SCHEMA_NAME=" + "'" + TableName + "'"
if res , _ := db.Query(SqlQuery1); res != nil{
return true, nil
}
//创建表
//CREATE TABLE table_name (name type ,...PRIMARY KEY ( _id ))ENGINE=InnoDB DEFAULT CHARSET=utf8;
var str1 , strkey string
for i := 0; i < TypeRef.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := TypeRef.Field(i)
// 输出成员名和tag
Printf("name: %v tag: '%v'
", fieldType.Name, fieldType.Tag)
//还需做一个默认值map
str1 +=fieldType.Tag.Get("column") + " , " + fieldType.Tag.Get("type")
if fieldType.Tag.Get("primary_key") != "" {
strkey = fieldType.Tag.Get("primary_key")
}
}
str1 += "PRIMARY KEY (" + strkey +")"
SqlQuery2 := "CREATE TABLE " + TableName + "(" + str1 + ")ENGINE=InnoDB DEFAULT CHARSET=utf8;"
if _, err := db.Exec(SqlQuery2); err != nil {
return false , err
}
return true , nil
}
Create
Create(Myobject interface{})(bool, error)
与上一个方法类似,但同时会插入与实例内容对应的字段。相当于创建并插入。返回值与上一样
func (b *orm )Create(Myobject interface{})(bool, error) {
db := b.db
ValueRef := reflect.ValueOf(Myobject).Elem()
TypeRef := ValueRef.Type()
s := TypeRef.Name()
TableName := strings.ToUpper(s[:0]) + s[1:] + "s"
SqlQuery1 := "SELECT information_schema.SCHEMATA.SCHEMA_NAME FROM information_schema.SCHEMATA where SCHEMA_NAME=" + "'" + TableName + "'"
if res , _ := db.Query(SqlQuery1); res != nil{
return true, nil
}
//CREATE TABLE table_name (name type ,...PRIMARY KEY ( _id ))ENGINE=InnoDB DEFAULT CHARSET=utf8;
var str1 , strkey string
var name , value []string
for i := 0; i < TypeRef.NumField(); i++ {
// 获取每个成员的结构体字段类型
fieldType := TypeRef.Field(i)
//还需做一个默认值map
str1 +=fieldType.Tag.Get("column") + " , " + fieldType.Tag.Get("type")
if fieldType.Tag.Get("primary_key") != "" {
strkey = fieldType.Tag.Get("primary_key")
}
//获取字段名以及插入的值
name = append(name, fieldType.Tag.Get("column"))
value = append(value, ValueRef.Field(i).String())
}
str1 += "PRIMARY KEY (" + strkey +")"
SqlQuery2 := "CREATE TABLE " + TableName + "(" + str1 + ")ENGINE=InnoDB DEFAULT CHARSET=utf8;"
if _, err := db.Exec(SqlQuery2); err != nil {
return false , err
}
//INSERT INTO %s(%s) VALUES(%s)
SqlQuery3 := Sprintln(`insert into %s (%s) values %s`, TableName , strings.Join(name,","), strings.Join(value,","))
if res , _ := db.Query(SqlQuery3); res != nil{
return true, nil
}
return true, nil
}
删
DeleteAll
DeleteAll(whereExpression string, params...interface{}, Myobject interface{}) error
根据传入的Myobject找到对应的表,然后删除符合条件的所有字段,即批量删除
如果表不存在,返回一个错误
Delete
Delete(Myobject interface{}) error
单个删除,默认通过Myobject的主键删除
返回值同上
改
Update
Update(Myobject) (bool,error)
利用主键寻找字段
整体更新传入的字段,更新成功返回一个bool值,找不到该表或者字段不吻合,则返回一个错误
查
FindOne
FindOne(whereExpression string , params...interface{},Myobject interface{})(interface{},error)
根据查询条件和实例找到目标表中的一个字段,返回对应生成的实例(map)。
如果找不到就返回一个错误。
func (b *orm)FindOne(Myobject interface{}) error {
db := b.db
//取值和取类型,传入的Myobject应该是接口类型的指针
TypeRef := reflect.TypeOf(Myobject)
ValueRef := reflect.ValueOf(Myobject)
//判断传入的值是否有效
if TypeRef.Kind() != reflect.Ptr {
return errors.New("should transport a ptr type")
}
if ValueRef.Elem().CanAddr() == false || ValueRef.Elem().CanSet() == false {
return errors.New("such ptr cannot be modified")
}
//构造SQL
TableName := GetTableName(Myobject)
SqlQuery := Sprintln("SELECT * FORM %s WHERE (%s) LIMIT 1",TableName , b.wheres)
var addr []interface{}
//ValueRef.Elem()为接口类型,这里要遍历接口的元素的地址,并存入addr数组
for i := 0; i < ValueRef.Elem().NumField(); i++ {
t := ValueRef.Field(i).Addr().interface()//地址也要注意以接口类型返回
addr = append(addr, t)
}
rows, err := db.Query(SqlQuery);
if err != nil{
return err
}
//将查询的值赋给addr,也相当于传回了Myobject
if err := rows.Scan(addr...);err != nil{
return err
}
return nil
}
FindAll
FindAll(whereExpression string , params...interface{},Myobject interface{})([]interface{},error)
根据查询条件和实例找到目标表中的所有字段,返回对应生成的实例数组(map)。
如果找不到就返回一个错误。
func (b *orm)Find(Myobject []interface{}) error {
db := b.db
//虽然Myobject是接口数组,但这里没有改动
TypeRef := reflect.TypeOf(Myobject)
ValueRef := reflect.ValueOf(Myobject)
if TypeRef.Kind() != reflect.Ptr {
return errors.New("should transport a ptr type")
}
if ValueRef.Elem().CanAddr() == false || ValueRef.Elem().CanSet() == false {
return errors.New("such ptr cannot be modified")
}
TableName := GetTableName(Myobject)
SqlQuery := Sprintln("SELECT * FORM %s WHERE (%s) ",TableName , b.wheres)
rows, err := db.Query(SqlQuery)
if err != nil{
return err
}
//返回多值,Myobject是接口数组,因此要多些一层反射
v := ValueRef.Elem()//v是传入的数组
i := 0
for rows.Next() {
t := v.Field(i) //t是数组元素,即结构体
i += 1
var addr []interface{}
for j :=0 ; j < t.Elem().NumField(); j ++ {
tt := t.Field(i).Addr().Interface()//tt为结构体的元素的地址
addr = append(addr, tt)
}
if err := rows.Scan(addr...);err != nil{
return err
}
}
return nil
}
进阶
链式操作(where)
type orm struct {
db *sql.DB
wheres string
}
//传入where的参数应该如下
//where("id = ? and price in (?)",myObject.ID,[]int {1,2})
//故只需做问号替换
func (b *orm)where(expression string, params...interface{}) *orm {
var res string
var j = 0
for i := 0 ; i < len(expression) ; i ++ {
res += string(expression[i])
if expression[i] == '?' {
res += format(reflect.ValueOf(params[j]))
j += 1
}
}
b.wheres = res
return b
}
//format用来调整sql where字句字符串格式,用的是别人的代码
func format(v reflect.Value) string {
//断言出time类型直接转unix时间戳
if t, ok := v.Interface().(time.Time); ok {
return Sprintf("FROM_UNIXTIME(%d)", t.Unix())
}
switch v.Kind() {
case reflect.String:
return Sprintf(`'%s'`, v.Interface())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return Sprintf(`%d`, v.Interface())
case reflect.Float32, reflect.Float64:
return Sprintf(`%f`, v.Interface())
//如果是切片类型,遍历元素,递归格式化成"(, , , )"形式
case reflect.Slice:
var values []string
for i := 0; i < v.Len(); i++ {
values = append(values, format(v.Index(i)))
}
return Sprintf(`%s`, strings.Join(values, ","))
//接口类型剥一层递归
case reflect.Interface:
return format(v.Elem())
}
return ""
}
想法
我感觉把ORM这个映射做成一个结构体来替代db,也即把这些CRUD方法实现在Myobject上不是更能体现ORM的意思吗,会不会更直观呢?然后Myobject里就包含了数据库对应的表名,和字段结构体。
不熟go语言。不熟反射,不熟。。。太难了