基于《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