最近一直没更新是因为在开发一个人才库系统,这个项目很简单,本质上就是个只有两张表的增删改查,但是却做了两周,这篇文章就是用来记录这次开发遇到的坑

程序设计

整个系统使用的技术也比较简单

前端:baidu-amis
服务端:golang gin sqlx mysql5.7

任务是按阶段来给的,这个阶段的功能大致就是:

实现一个对 候选人面试记录 的增删改查功能

数据库设计

候选人与面试记录是 1:N 的关系,所以数据库的设计也就是两张表:candidate 候选人表 和 feedback 面试反馈表,通过在 feedback 记录中记录所从属的 candidate 记录主键来实现两表之间的关联

对象设计

在对象设计这里,我纠结了很久,由于之前写的 java,这是我使用 golang 做的第一个正式项目,把不少之前 java 的编码习惯带了过来,虽然在编码过程中尽力去避免这种情况,最后从成品来看,还是一股子java 味儿……

一共 3 个 model,分别是:对应 candidate 表的对象 Candidate、对应 feedback 表的对象 Feedback 和用于接收来自页面参数和用于页面展示的对象 Candidateview

三者的对应关系如下:

type Feedback struct {
// feedback properties...
}

type Candidate struct {
// candidate properties...
Feedbacks []Feedback
}

type CandidateView struct {
*Candidate
// params... 页面参数
// convert types... 转换对象,这个后面说
}

纠结点:

  1. 话说我在对象、属性命名的时候纠结了好久,这也是浪费了大量时间的原因,应该在一开始就统一命名规则,目前项目内变量和函数命名还未统一,等今后重构的时候再来解决吧……
  2. 关于对象的设计形式,开始的时候我就在想,我是应该面向数据库来设计对象呢,还是面向表单来设计对象呢?最后采取了上面这种融合的形式,对象面向数据库,通过内置对象的形式来组合出表单的对象(也就是 candidateview)

程序结构设计

做过 java 应该对 controller-service-dao-entity 的层级非常熟悉,这个项目当前也是这样设计的

下面介绍一下各个层级的分工(命名方式与上面不同,但意思是一样的)

  1. model:参数存储容器,同时提供给 repository 实现增删改查的sql语句
  2. repository:连接数据库,使用 model 提供的 sql 实现增删改查功能
  3. service:从 repository 获取增删改查的结果,将其封装成返回给 api 层的 json 数据
  4. api:从页面接收数据并封装成 CandidateView 对象,传向 service

emm,看完是不是有种 过度设计 的感觉?

对象转换

因为候选人的年龄是会随着时间发生变化的,所以在数据库中,存储的并不是他们的年龄,而是他们的生日,所以在页面展示时,是需要通过他们的生日计算出年龄用于显示的,所以举一个例子:

Candidate 中有一个属性对应候选人的生日,在 repository 中将他的数据查询后并填入 Candidate 对象后,需要使用函数将 Candidate
类型 time.Time 的属性 Birthday 计算得出 CandidateView 中的 int 类型 Age 对象

下面是具体实现

func (cv *CandidateView) Birthday2Age() {
	now := time.Now()
	age := now.Year() - cv.Birthday.Year()
	if now.Month() > cv.Birthday.Month() || now.Month() == cv.Birthday.Month() && now.Day() > cv.Birthday.Day() {
		age++
	}
	cv.Age = age
}

这一步是在 repository 层中将单行数据取出后完成的

事实上,在项目中这样的对象转换并不止一个

再比如说 baidu-amis 里的时间控件传回 api 时是时间戳字符串,但是由于 Candidate 中的 Birthday 字段是 time.Time 并不能自动完成赋值(即 gin.ShouldBind),所以有个中间字段 BirthStamp

对象转换函数

// 将表单时间戳转为生日对象
func (cv *CandidateView) BirthStamp2Time() {
	if cv.BirthdayStamp != "" {
		stamp, _ := strconv.ParseInt(cv.BirthdayStamp, 10, 64)
		cv.Birthday = time.Unix(stamp, 0)
	}
}

// 将db中time.time转时间戳
func (cv *CandidateView) Time2BirthStamp() {
	cv.BirthdayStamp = strconv.FormatInt(cv.Birthday.Unix(), 10)
}

嗯,很不优雅,简直丑爆了……

事务处理

除了查询外的操作都应该加上 事务,为了避免有多个 err 写多个 ifrollback 的情况出现,我的处理代码如下:

func Xxx(cv *model.CandidateView) error{
	var err error
	tx,_ := db.Beginx()
	defer tx.Rollback()
	if err = tx.NamedExec(sql1,cv);err != nil {
		log.Println(err)
		return err
	}
	if err = tx.NamedExec(sql2,cv);err != nil {
		log.Println(err)
		return err
	}
	...
	err = tx.commit()
	return err
}

如果你有更好的写法请留言评论~

数据库连接链接

mysql 中取出的 date 类型字段并不会自动被转换成 time.Time 赋值给对应字段,需要在数据库连接链接后加上一段 ?parseTime=true

例如:root:@tcp(127.0.0.1:3306)/db_name?parseTime=true

关于重构

项目在刚开始就经历了多次重构,如前面所说,就是为了避免项目太 java,可是到了后面还是写的很 java,需要多看看其他的 golang 项目再来改进写法

至少有一点可以确认的是,Golang 是一门很简洁的语言,能在很短的时间就重构的面目全非,像上面这么设计肯定是过度设计了……

ps: 以后再做开发要自上而下设计,从功能实现逻辑 到 界面(反正用 amis,界面都差不多的) 到 代码结构设计(把函数名、函数参数、函数返回值写好,函数体里写 TODO)

Q.E.D.


此 生 无 悔 恋 真 白 ,来 世 愿 入 樱 花 庄 。