最近一直没更新是因为在开发一个人才库系统,这个项目很简单,本质上就是个只有两张表的增删改查,但是却做了两周,这篇文章就是用来记录这次开发遇到的坑
程序设计
整个系统使用的技术也比较简单
前端: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... 转换对象,这个后面说
}
纠结点:
- 话说我在对象、属性命名的时候纠结了好久,这也是浪费了大量时间的原因,应该在一开始就统一命名规则,目前项目内变量和函数命名还未统一,等今后重构的时候再来解决吧……
- 关于对象的设计形式,开始的时候我就在想,我是应该面向数据库来设计对象呢,还是面向表单来设计对象呢?最后采取了上面这种融合的形式,对象面向数据库,通过内置对象的形式来组合出表单的对象(也就是
candidateview
)
程序结构设计
做过 java
应该对 controller-service-dao-entity
的层级非常熟悉,这个项目当前也是这样设计的
下面介绍一下各个层级的分工(命名方式与上面不同,但意思是一样的)
model
:参数存储容器,同时提供给repository
实现增删改查的sql语句repository
:连接数据库,使用model
提供的 sql 实现增删改查功能service
:从repository
获取增删改查的结果,将其封装成返回给api
层的 json 数据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
写多个 if
和 rollback
的情况出现,我的处理代码如下:
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.