查询时避免返回对象初始化及sql.ErrNoRows判断

执行SQL查询时,请避免提前将查询结果初始化,以避免结构对象默认值的影响,避免创建不必要的对象内存。通过返回对象指针nil判断避免sql.ErrNoRows使用,降低代码对error处理的复杂度、统一项目中对空查询结果处理逻辑。

一个反面例子:

  1. func (s *sTask) GetOne(ctx context.Context, id uint64) (out *entity.ResourceTask, err error) {
  2. out = new(model.TaskDetail)
  3. err = dao.ResourceTask.Ctx(ctx).WherePri(id).Scan(out)
  4. if err != nil {
  5. if err == sql.ErrNoRows {
  6. err = gerror.Newf(`record not found for "%d"`, id)
  7. }
  8. return
  9. }
  10. return
  11. }

在该例子中,实际返回的out对象由于对象初始化的缘故有了默认值(无论SQL是否查询到数据),并且sql.ErrNoRows的判断增加了代码逻辑中对error处理的复杂度。

建议改进如下:

  1. func (s *sTask) GetOne(ctx context.Context, id uint64) (out *entity.ResourceTask, err error) {
  2. err = dao.ResourceTask.Ctx(ctx).WherePri(id).Scan(&out)
  3. if err != nil {
  4. return
  5. }
  6. if out == nil {
  7. err = gerror.Newf(`record not found for "%d"`, id)
  8. }
  9. return
  10. }

注意代码中&out的使用。

更多的介绍请参考:ORM结果处理-为空判断

复杂类型尽量使用json存储,便于Scan到对象时自动化转换,避免自定义解析

举一个🌰。假如我们需要实现产品售卖规格列表,其中包含可选择的分片数量、分片容量以及副本数量,如下图(非现网代码,仅供示例学习):

ORM最佳实践 - 图1

我们的表设计如下:

  1. CREATE TABLE `sell_spec` (
  2. `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. `product` varchar(45) NOT NULL COMMENT '产品名称',
  4. `resources` json NOT NULL COMMENT '资源规格(cpu:memory),例如:["0:0.25", "0:1", "1:2"]',
  5. `disk_min` int(10) DEFAULT NULL COMMENT '磁盘最小容量',
  6. `disk_max` int(10) DEFAULT NULL COMMENT '磁盘最大容量',
  7. `disk_step` int(10) DEFAULT NULL COMMENT '磁盘递增大小',
  8. `shards` json NOT NULL COMMENT '分片规格,例如:[1,3,5,8,12,16,24,32,40,48,64,80,96,128]',
  9. `replicas` json NOT NULL COMMENT '副本规格,例如:[1,2,3,4,5,6,7,8,9,12]',
  10. `created_at` datetime DEFAULT NULL COMMENT '创建时间',
  11. `updated_at` datetime DEFAULT NULL COMMENT '更新时间',
  12. PRIMARY KEY (`id`)
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='售卖规格配置';

其中的resources, shards, replicas我们定义为json格式,目的是可以存储自定义的资源、分片、副本规格列表(非顺序性)。那么我们的go struct定义如下:

  1. // SellSpec 是通过GoFrame工具自动生成的数据结构,由工具维护。
  2. type SellSpec struct {
  3. Id uint `description:"主键"`
  4. Product string `description:"产品名称"`
  5. Resources string `description:"资源规格(cpu:memory),例如:[\"0:0.25\", \"0:1\", \"1:2\"]"`
  6. DiskMin int `description:"磁盘最小容量"`
  7. DiskMax int `description:"磁盘最大容量"`
  8. DiskStep int `description:"磁盘递增大小"`
  9. Shards string `description:"分片规格,例如:[1,3,5,8,12,16,24,32,40,48,64,80,96,128]"`
  10. Replicas string `description:"副本规格,例如:[1,2,3,4,5,6,7,8,9,12]"`
  11. CreatedAt *gtime.Time `description:"创建时间"`
  12. UpdatedAt *gtime.Time `description:"更新时间"`
  13. }
  14. // SellSpecItem 是扩展entity的自定义数据结构,其中部分字段被覆盖为了数组类型。
  15. type SellSpecItem struct {
  16. entity.SellSpec
  17. Resources []string `dc:"资源规格"`
  18. Shards []int `dc:"分片规格"`
  19. Replicas []int `dc:"副本规格"`
  20. }

那么在程序中我们可以这么来写入和查询数据记录。

数据写入:

  1. _, err = dao.SellSpec.Ctx(ctx).Data(v1.SellSpecItem{
  2. SellSpec: entity.SellSpec{
  3. Product: "redis",
  4. DiskMin: 50,
  5. DiskMax: 1000,
  6. DiskStep: 10,
  7. },
  8. Resources: []string{"1:2", "2:4", "4:8"},
  9. Shards: []int{1, 3, 5, 8, 12, 16, 24, 32, 40, 48, 64, 80, 96, 128},
  10. Replicas: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 12},
  11. }).Insert()

数据查询

  1. var items []v1.SellSpecItem
  2. err = dao.SellSpec.Ctx(ctx).Scan(&items)