古老的榕树

用 Go 开发终端接口服务--定义 model 实体层结构体

潘军杰 发表于 2019-05-14 16:37 阅读(4267) 评论(0) 赞(0)
本教程主要讲解项目的构建方法,并不会涉及错综复杂的业务,错综复杂的业务是由多个业务实体和多个实体关系组成的,我们进阶教程会讲解到,所以我们项目的实体也是简单的,现在我们主要挑选典型的 Product 产品和 Product  Photo 产品图片业务进行讲解,把构建的方法和流程说明清楚,其他业务模块因项目不同而异,但再复杂的项目,万变不离宗,只要我们掌握了方法,完全可以掌握构建更复杂的项目。

在讲解 model 实体层之前,需要列出数据库的结构。我们数据库新建一个 chapter01 的数据库,并新建 pproduct 产品表和 product_photo 产品图片两张表,在客户端查询窗口上执行以下 SQL 语句,完成建库建表操作。

*代码清单 - 数据库完整的 SQL 语句*

-- 先新建一个名为 chapter01 的数据库
CREATE SCHEMA `chapter01` DEFAULT CHARACTER SET utf8mb4 ;

-- 新建 product 表
CREATE TABLE `product` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增 ID',
  `category` bigint(20) unsigned DEFAULT '0' COMMENT '分类',
  `name` varchar(45) DEFAULT '' COMMENT '产品名称',
  `intro` varchar(255) DEFAULT '' COMMENT '产品简介',
  `price` decimal(18,2) DEFAULT '0.00',
  `status` smallint(5) unsigned DEFAULT '1' COMMENT '状态 1:可用;2:不可用;',
  `created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '新建时间',
  `updated` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';

-- 新建 product_photo 表
CREATE TABLE `product_photo` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增 ID',
  `product_id` bigint(20) unsigned DEFAULT '0' COMMENT '关联的产品 ID',
  `path` varchar(255) DEFAULT '' COMMENT '后缀路径',
  `seq` int(10) unsigned DEFAULT '1' COMMENT '序号',
  `created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '新建时间',
  `updated` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_product_photo_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品图片';


以上 SQL 脚本,仔细观察的同学,会发现表全部字段都有默认值,因为 Go 数据类型是有默认值的,我们建表结合 Go 的数据类型来做的,否则数据库返回的某个字段出现 null 值,扫描赋值给 Go 结构体属性的时候,会发生异常。数据如果是数值类型(如:int、float、double、float、decimal),默认值指定零值(如 0、0.00),字符串类型指定空字符串。所以这么约定好规则,可以避免没必要的问题发生,另外给数据指定显性的默认值,也是有好处的,这样可以使数据库数据结构更加清晰,不容易产生歧义。

*注:MySQL 的 text、longtext 默认值是不能指定空字符串的,这时我们定义 Go 结构体对应属性的时候,可以使用 sql.NullString 类型了,它是一个复合类型,取值的时候稍微注意一下。*

在 chapter01/src/model 包上,新建 product.go 和 product_photo.go 两个文件,加上之前新建的 init.go 一共三个文件,init.go 里定义初始化函数和一部分公共结构体。

其余两个文件定义的结构体和数据库的表是相互对应的,product 产品表和 product_photo 产品图片表,也分别对应两个结构体,我们命名是也是对应的,数据库一般是使用下划线命名方式,Go 结构体的命名采用驼峰命名方式,这是惯用的约定规则。

*代码清单 - 主业务实体结构体*

// Product 产品结构体
type Product struct {
	BaseModel
	Category int64   `db:"category" json:"category"`
	Name     string  `db:"name" json:"name"`
	Intro    string  `db:"intro" json:"intro"`
	Price    float64 `db:"price" json:"price"`
	Status   int     `db:"status" json:"status"`
}

// ProductPhoto 产品图片的结构体
type ProductPhoto struct {
	BaseModel
	ProductID int64  `db:"product_id" json:"productID"`
	Path      string `db:"path" json:"path"`
	Seq       int    `db:"seq" json:"seq"`
}


实体结构体除了主业务实体结构体和数据库表相对应,项目还需要一些辅助的结构体,用于用于请求或响应时承载数据,比如 上传图片所请求的参数和返回的数据,显示图片所需要的 URL、Path 字段,实体的基类结构体等。

*代码清单 - 辅助实体结构体*
// ProductExt 产品扩展结构体
type ProductExt struct {
	Product
	Photos []ViewPhotoRespArgs `json:"photos"`
}

// ProductPhoto 产品图片的结构体
type ProductPhoto struct {
	BaseModel
	ProductID int64  `db:"product_id" json:"productID"`
	Path      string `db:"path" json:"path"`
	Seq       int    `db:"seq" json:"seq"`
}

// ViewPhotoRespArgs 查看图片响应的结构体
type ViewPhotoRespArgs struct {
	ID   int64  `db:"id" json:"id,omitempty"`
	Path string `db:"path" json:"path"`
	URL  string `db:"url" json:"url"`
	Seq  int    `db:"seq" json:"seq"`
}

// UploadFileArgs 上传文件的请求结构体
type UploadFileArgs struct {
	File    []byte `json:"file"`
	FileExt string `json:"fileExt"`
	Seq     int    `json:"seq"`
}

// UploadPhotoRespArgs 完成上传图片后的响应结构体
type UploadPhotoRespArgs struct {
	Src   string `json:"src"`
	Small string `json:"small"`
	Big   string `json:"big"`
	Cut   string `json:"cut"`
	Seq   int    `json:"seq"`
}

实体层的包其实有若干 go 文件组成的,他们分别是 init.go、product.go、product_photo.go。它们根据业务类型,分开文件存储,实际编译后,该包的几个文件都会合并一起。

// JSONTime 重写了 time.Time JSON 的序列函数
type JSONTime time.Time

// String  打印输出字符串
func (t JSONTime) String() string {
	return time.Time(t).Format("2006-01-02 15:04:05")
}

// MarshalText 序列化
func (t JSONTime) MarshalText() ([]byte, error) {
	return []byte(`"` + t.String() + `"`), nil
}

// MarshalJSON JSON 序列化输出
func (t JSONTime) MarshalJSON() ([]byte, error) {
	return t.MarshalText()
}

// UnmarshalJSON JSON 反序列化
func (t *JSONTime) UnmarshalJSON(data []byte) error {
	dt, err := time.ParseInLocation(`2006-01-02 15:04:05`, string(data), time.Local)
	if err != nil {
		return err
	}

	*t = JSONTime(dt)
	return nil
}

// BaseModel 基类结构体
type BaseModel struct {
	ID      int64     `db:"id" json:"id"`
	Created *JSONTime `db:"created" json:"created"`
	Updated *JSONTime `db:"updated" json:"updated"`
}


以上代码定义了 JSONTime 类型,是扩展了 time.Time 类型,主要为接口响应输出 JSON 的时候,时间字段格式化为 yyyy-MM-dd HH:mm:ss 。

而 BaseModel 是基类结构体,只有三个字段 ID Created Updated 也是所有的结构体都有的字段,把他们抽出来,独立定义成基类,嵌套(继承)到其他结构体里,可以减少一定的代码量。

service 服务层返回的数据,是非常关键的数据结构,是最常用的对象,所以 model 实体层我们还定义了 ServiceResponse 结构体,就是 service 层专门给 controller 层接口返回响应数据使用的,ServiceResponse 的函数写在 service 服务层里 ,为了讲解的需要,在此我们把它们放在一起了。

// ServiceResponse 响应主体结构体
type ServiceResponse struct {
	Code     int         `json:"code"`
	ErrorMsg string      `json:"errorMsg"`
	Body     interface{} `json:"body,omitempty"`
}

// 以下代码 service 属于服务层的代码
// serviceResponseSuccess 只简单返回成功
func ServiceResponseSuccess(body ...interface{}) model.ServiceResponse {
	return SetServiceResponseCode(common.CodeSuccess, body...)
}

// serviceResponseFailure 只简单返回操作失败
func ServiceResponseFailure() model.ServiceResponse {
	return SetServiceResponseCode(common.CodeFailure)
}

// setServiceResponseCode 响应主体结构体
func SetServiceResponseCode(resultCode int, body ...interface{}) model.ServiceResponse {
	return SetServiceResponse(resultCode, common.CodeMsgMap[resultCode], body...)
}

// setServiceResponse 响应主体结构体
func SetServiceResponse(resultCode int, errMsg string, body ...interface{}) (respBody model.ServiceResponse) {
	respBody.Code = resultCode
	respBody.ErrorMsg = errMsg
	if len(body) > 0 {
		respBody.Body = body[0]
	}
	return respBody
}
ServiceResponse 结构体,项目的主要目标是返回 ServiceResponse  实例给终端,ServiceResponse 包含了几个最常用的字段 Code, ErrorMsg, Body ,其中 Body 是 interface{} 接口类型,它可能是 slice 类型的集合数据(多条数据),也有可能是一个实体数据(一条数据),或者 body 就是空数据,不承载任何数据,所以 Body 终将变得非常灵活,我们在 service 服务层章节再具体说明一下,结构体内置了几个返回 ServiceResponse 实例的函数,给 service 服务层快捷调用它们,得到 ServiceResponse 实例返回给 controller 控制层,再转成 JSON 返回给终端。

小结
model 实体层的结构体,有些是和数据库表对应的,也有些是基类或扩展结构体,比如 BaseModel 就是基类结构体,里面有公共的字段 ID,Created,Updated(自增 ID、新建时间、最后更新时间)三个字段,因为我们每个表都有这三个字段,对应的结构体也一样的,如果这些结构体嵌套上 BaseModel ,那么就不用都一一写上这三个字段了;而扩展结构体,也是给其他业务使用,比如我们返回参数,请求参数,比如 ServiceResponse 就是服务层返回的结构体,和数据库表不相干的,还有 UploadFileArgs 结构体是图片上传的时候用到的。



《用 Go 开发终端接口服务》 目录


0 条网友评论

哇~~~ 竟然还没有评论!

称呼*
邮箱*
内容*
验证码*
验证码 看不清换张