gorm文章、标签多对多create实现过程记录(包含踩坑)

基于《Go语言编程实战》第二章课后作业,做出tag和article的多对多关系映射

书中所写一对多感觉不准确,关系应该说多对多的关系。书中已经提前建立了blog_article_tag表,本文选择将表删掉,通过gorm标签建立多对多映射关系,自动生成数据表

首先在字段中添加多对多映射的部分:

type Article struct {
	*Model
	Title         string `json:"title"`
	Desc          string `json:"desc"`
	Content       string `json:"content" gorm:"type:longtext"`
	CoverImageUrl string `json:"cover_image_url"`
	State         uint8  `json:"state"`
	Tags          []Tag  `json:"tags" gorm:"many2many:blog_tag_article"`
}

type Tag struct {
	Model
	Name     string    `json:"name"`
	State    uint8     `json:"state"`
	Articles []Article `json:"articles" gorm:"many2many:blog_tag_article"`
}

在model的struct定义里,向两个struct里写上包含对方的struct列表。在后面gorm中声明“many2many:blog_tag_article”,blog_tag_article即将要建立的连接表的名称。

完成上述步骤后,在数据库初始化代码中加入db.AutoMigrate(&Article{},&Tag{})。AutoMigrate会根据struct声明的字段构造对应的数据库表,Article中指定了gorm:"type:longtext"可以直接设定数据库表中对应字段的类型,如果不设定则默认根据struct对应类型映射

更改请求接口struct部分代码:

type ArticleCreateRequest struct {
	Title    string `form:"title" binding:"required,min=1,max=100"`
	Desc     string `form:"desc" binding:"required,min=2,max=255"`
	Content  string `form:"content" binding:"required,min=1"`
	CreateBy string `form:"create_by" binding:"required,min=2,max=100"`
	State    uint8  `form:"state,default=1" binding:"oneof=0 1"`
	Tags     string `form:"tags" binding:"required,max=255"`
}

这里采用的方式是,在最后增加Tags字段,以string的方式传入,传入格式为“1-2-3-4”(代表标签id为1,2,3,4),在services层使用split处理为列表

func (svc *Service) CreateArticle(param *ArticleCreateRequest) error {
	tagList := strings.Split(param.Tags, "-")
	return svc.dao.CreateArticle(param.Title, param.Desc, param.Content, param.CreateBy, param.State, tagList)
}

更改dao层创建文章代码:

func (d *Dao) CreateArticle(title string, desc string, content string, create_by string, state uint8, tagListStr []string) error {
	var tagList []model.Tag
	var tag model.Tag
	for _, tagName := range tagListStr {
		tagid := convert.SrcTo(tagName).MustInt()
		//queryDB := d.engine.Where("is_del=?",0).Session()
		//d.engine.Where("id=?", tagid).Find(&tag)
		d.engine.Debug().Raw("select * from blog_tag where id = ?", tagid).Scan(&tag)
		tagList = append(tagList, tag)
	}
	for _, tagName := range tagList {
		fmt.Println(tagName.ID)
	}

	article := model.Article{
		Title:   title,
		Desc:    desc,
		Content: content,
		State:   state,
		Model:   &model.Model{CreateBy: create_by},
		Tags:    tagList,
	}
	return article.Create(d.engine)
}

一:理论上,在automigrate中已经建立了多对多关系,所以在创建新的article的时候,在对应的Tags字段输入对应的tag结构体即可自动插入文章表、标签表、以及标签文章关联表相关信息。但是如果按照标签名称插入:

article := model.Article{
		Title:   title,
		Desc:    desc,
		Content: content,
		State:   state,
		Model:   &model.Model{CreateBy: create_by},
		Tags:    model.Tag{Name:tag1}
	}

由于Name不是主键,所以如果文章1和文章2都有tag1,会造成在tag表中创建两个tag1,没有实现真正的多对多关系

于是选择使用标签主键进行映射,但是又产生问题,通过多对多映射生成的标签很多字段默认为空或者0,导致state默认为0,检索的时候没有办法检索到。

整个项目实现的思路应该为:服务端向客户提供文章标签,客户选择文章标签,在代码内部处理文章标签部分为对应的请求,我们这里只是模拟内部请求(感觉涉及到前端接口之类的问题了),最重要的是:tag表中的tags应该预先建好,通过tag的api建立

mysql> select * from blog_tag;
+----+------------+-------------+------------+-------------+------------+--------+--------+-------+
| id | create_by  | modified_by | created_on | modified_on | deleted_on | is_del | name   | state |
+----+------------+-------------+------------+-------------+------------+--------+--------+-------+
|  1 |            |             |          0 |  1685956876 |          0 |      0 |        |     0 |
|  2 |            |             |          0 |  1685956876 |          0 |      0 |        |     0 |
|  3 |            |             | 1685952118 |  1685956876 |          0 |      0 |        |     0 |
|  4 | temptation |             | 1685953063 |  1685956876 |          0 |      0 | tag666 |     1 |
|  5 | temptation |             | 1685956337 |  1685956876 |          0 |      0 | tag5   |     1 |
+----+------------+-------------+------------+-------------+------------+--------+--------+-------+

1、2、3是多对多关系中生成的,字段很多为0,4、5是通过api生成的,可用

再解释一下上面所说的“1、2、3是多对多关系中生成的”,一开始实现方式是在Article中生成新的tag实体插入taglist中进行建库,这样就自动生成了id123的条目,没有创建时间和state=1,并且无法建立关联。因为article实体中加入的Tags必须是从数据库中查询得到的对象,即已经存在于数据库中的条目。所以采用的方法是按照输入的tagid,从tag表中查到对应的条目插入taglist中,再随着article的建立构造映射关系

坑二:本来是通过for循环,db.engine.Where.First直接进行的查询,但是由于查询条件污染,后面的查询会带有前面所有查询成功的where条件。官方文档提供了新建Session的方法,但是不知道为什么db里没有对应的Session函数。

解决办法:直接使用原始sql语句进行查询:

for _, tagName := range tagListStr {
		tagid := convert.SrcTo(tagName).MustInt()
		//queryDB := d.engine.Where("is_del=?",0).Session()
		//d.engine.Where("id=?", tagid).Find(&tag)
		d.engine.Debug().Raw("select * from blog_tag where id = ?", tagid).Scan(&tag)
		tagList = append(tagList, tag)
	}

坑三:如果文章带有1,2,3,4四个标签,最后得到的映射关系只有4(最后一个)

+------------+--------+
| article_id | tag_id |
+------------+--------+
|         11 |      5 |
|         12 |      1 |
|         12 |      2 |
|         12 |      3 |
|         12 |      4 |
|         12 |      5 |

这里就是插入文章11的时候,只建立起来和tag5的映射。

原因是tag中的model一开始是*Model,可能因为指针传递等问题,导致最后所有的tag都成为了id为5的tag,将model.go里的tag中的*Model改为Model即可(即指针传递改为值传递),问题解决

请求格式 :   curl -X POST 'http://127.0.0.1:8080/api/v1/articles' -F "title={title}" -F "create_by={creater}" xxxxxxxxx.  -F "tags=1-2-3-4-5"(即给这个文章添加12345标签)

同时 更改get部分代码,加入预加载部分:

func (a Article) Get(db *gorm.DB) (*Article, error) {
	article := &Article{}
	db = db.Where("is_del=?", 0)

	if err := db.Preload("Tags").First(article, a.ID).Error; err != nil {
		return nil, err
	}
	return article, nil
}

func (t Tag) List(db *gorm.DB, pageOffset, pageSize int) ([]*Tag, error) {
	var tags []*Tag
	var err error
	if pageOffset >= 0 && pageSize > 0 {
		db = db.Offset(pageOffset).Limit(pageSize)
	}
	if t.Name != "" {
		db = db.Where("name = ?", t.Name)
	}
	db = db.Where("state = ?", t.State)
	if err = db.Preload("Articles").Where("is_del = ?", 0).Find(&tags).Error; err != nil {
		return nil, err
	}

	return tags, nil
}

因为默认查询情况下,struct list部分的属性默认是空的,所以需要查询时候Preload(struct list字段名),在查询的时候将其关联的信息也查询回来

查询效果:

root@iZ0jl9uy0ja8cnm78j65glZ:~/project/go_blog/go-programming-tour-book/blog-service# curl -X GET 'http://127.0.0.1:8080/api/v1/articles/12'
{"id":12,"create_by":"temptation","modified_by":"","created_on":1685956876,"modified_on":1685956876,"deleted_on":0,"is_del":0,"title":"testtitle12","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":[{"id":1,"create_by":"","modified_by":"","created_on":0,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"","state":0,"articles":null},{"id":2,"create_by":"","modified_by":"","created_on":0,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"","state":0,"articles":null},{"id":3,"create_by":"","modified_by":"","created_on":1685952118,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"","state":0,"articles":null},{"id":4,"create_by":"temptation","modified_by":"","created_on":1685953063,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"tag666","state":1,"articles":null},{"id":5,"create_by":"temptation","modified_by":"","created_on":1685956337,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"tag5","state":1,"articles":null}]}

反向查询效果:

curl -X GET 'http://127.0.0.1:8080/api/v1/tags'
{"list":[{"id":4,"create_by":"temptation","modified_by":"","created_on":1685953063,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"tag666","state":1,"articles":[{"id":6,"create_by":"temptation","modified_by":"","created_on":1685953162,"modified_on":1685953162,"deleted_on":0,"is_del":0,"title":"testtitle6","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":null},{"id":9,"create_by":"temptation","modified_by":"","created_on":1685955812,"modified_on":1685955812,"deleted_on":0,"is_del":0,"title":"testtitle9","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":null},{"id":10,"create_by":"temptation","modified_by":"","created_on":1685956211,"modified_on":1685956211,"deleted_on":0,"is_del":0,"title":"testtitle10","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":null},{"id":12,"create_by":"temptation","modified_by":"","created_on":1685956876,"modified_on":1685956876,"deleted_on":0,"is_del":0,"title":"testtitle12","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":null}]},{"id":5,"create_by":"temptation","modified_by":"","created_on":1685956337,"modified_on":1685956876,"deleted_on":0,"is_del":0,"name":"tag5","state":1,"articles":[{"id":11,"create_by":"temptation","modified_by":"","created_on":1685956361,"modified_on":1685956361,"deleted_on":0,"is_del":0,"title":"testtitle11","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":null},{"id":12,"create_by":"temptation","modified_by":"","created_on":1685956876,"modified_on":1685956876,"deleted_on":0,"is_del":0,"title":"testtitle12","desc":"testdesc","content":"testcontent","cover_image_url":"","state":1,"tags":null}]}],"pager":{"page":1,"page_size":10,"total_rows":2}}

可以通过tag查询到article,也可以通过article查询到tag

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
GORM是一个Go语言的ORM库,支持很多数据库,包括MySQL、PostgreSQL、SQLite等等。 在GORM中,一对多关系可以通过定义结构体中的slice来实现。例如,一个班级有多个学生,班级和学生之间就是一对多关系。可以这样定义: ```go type Class struct { gorm.Model Name string Students []Student } type Student struct { gorm.Model Name string ClassID uint } ``` 在上面的代码中,`Class`结构体中有一个`Students`字段,类型为`[]Student`,表示班级中有多个学生。而`Student`结构体中有一个`ClassID`字段,类型为`uint`,表示学生属于哪个班级。通过这种方式,就可以实现一对多关系。 多对多关系也可以通过定义结构体中的slice来实现。例如,一个学生可以选择多个课程,一个课程也可以被多个学生选择,学生和课程之间就是多对多关系。可以这样定义: ```go type Student struct { gorm.Model Name string Courses []Course `gorm:"many2many:student_courses;"` } type Course struct { gorm.Model Name string Students []Student `gorm:"many2many:student_courses;"` } ``` 在上面的代码中,`Student`结构体中有一个`Courses`字段,类型为`[]Course`,表示学生选择了哪些课程。而`Course`结构体中有一个`Students`字段,类型为`[]Student`,表示选择了该课程的学生。需要注意的是,在结构体的`gorm`标签中,需要指定`many2many`关系的表名,这里是`student_courses`。 以上就是在GORM实现一对多和多对多关系的方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值