package articles import ( "strconv" "github.com/gothinkster/golang-gin-realworld-example-app/common" "github.com/gothinkster/golang-gin-realworld-example-app/users" "gorm.io/gorm" ) type ArticleModel struct { gorm.Model Slug string `gorm:"uniqueIndex"` Title string Description string `gorm:"size:2048"` Body string `gorm:"size:2048"` Author ArticleUserModel AuthorID uint Tags []TagModel `gorm:"many2many:article_tags;"` Comments []CommentModel `gorm:"ForeignKey:ArticleID"` } type ArticleUserModel struct { gorm.Model UserModel users.UserModel UserModelID uint ArticleModels []ArticleModel `gorm:"ForeignKey:AuthorID"` FavoriteModels []FavoriteModel `gorm:"ForeignKey:FavoriteByID"` } type FavoriteModel struct { gorm.Model Favorite ArticleModel FavoriteID uint FavoriteBy ArticleUserModel FavoriteByID uint } type TagModel struct { gorm.Model Tag string `gorm:"uniqueIndex"` ArticleModels []ArticleModel `gorm:"many2many:article_tags;"` } type CommentModel struct { gorm.Model Article ArticleModel ArticleID uint Author ArticleUserModel AuthorID uint Body string `gorm:"size:2048"` } func GetArticleUserModel(userModel users.UserModel) ArticleUserModel { var articleUserModel ArticleUserModel if userModel.ID == 0 { return articleUserModel } db := common.GetDB() db.Where(&ArticleUserModel{ UserModelID: userModel.ID, }).FirstOrCreate(&articleUserModel) articleUserModel.UserModel = userModel return articleUserModel } func (article ArticleModel) favoritesCount() uint { db := common.GetDB() var count int64 db.Model(&FavoriteModel{}).Where(FavoriteModel{ FavoriteID: article.ID, }).Count(&count) return uint(count) } func (article ArticleModel) isFavoriteBy(user ArticleUserModel) bool { db := common.GetDB() var favorite FavoriteModel db.Where(FavoriteModel{ FavoriteID: article.ID, FavoriteByID: user.ID, }).First(&favorite) return favorite.ID != 0 } // BatchGetFavoriteCounts returns a map of article ID to favorite count func BatchGetFavoriteCounts(articleIDs []uint) map[uint]uint { if len(articleIDs) == 0 { return make(map[uint]uint) } db := common.GetDB() type result struct { FavoriteID uint Count uint } var results []result db.Model(&FavoriteModel{}). Select("favorite_id, COUNT(*) as count"). Where("favorite_id IN ?", articleIDs). Group("favorite_id"). Find(&results) countMap := make(map[uint]uint) for _, r := range results { countMap[r.FavoriteID] = r.Count } return countMap } // BatchGetFavoriteStatus returns a map of article ID to whether the user favorited it func BatchGetFavoriteStatus(articleIDs []uint, userID uint) map[uint]bool { if len(articleIDs) == 0 || userID == 0 { return make(map[uint]bool) } db := common.GetDB() var favorites []FavoriteModel db.Where("favorite_id IN ? AND favorite_by_id = ?", articleIDs, userID).Find(&favorites) statusMap := make(map[uint]bool) for _, f := range favorites { statusMap[f.FavoriteID] = true } return statusMap } func (article ArticleModel) favoriteBy(user ArticleUserModel) error { db := common.GetDB() var favorite FavoriteModel err := db.FirstOrCreate(&favorite, &FavoriteModel{ FavoriteID: article.ID, FavoriteByID: user.ID, }).Error return err } func (article ArticleModel) unFavoriteBy(user ArticleUserModel) error { db := common.GetDB() err := db.Where("favorite_id = ? AND favorite_by_id = ?", article.ID, user.ID).Delete(&FavoriteModel{}).Error return err } func SaveOne(data interface{}) error { db := common.GetDB() err := db.Save(data).Error return err } func FindOneArticle(condition interface{}) (ArticleModel, error) { db := common.GetDB() var model ArticleModel err := db.Preload("Author.UserModel").Preload("Tags").Where(condition).First(&model).Error return model, err } func FindOneComment(condition *CommentModel) (CommentModel, error) { db := common.GetDB() var model CommentModel err := db.Preload("Author.UserModel").Preload("Article").Where(condition).First(&model).Error return model, err } func (self *ArticleModel) getComments() error { db := common.GetDB() err := db.Preload("Author.UserModel").Model(self).Association("Comments").Find(&self.Comments) return err } func getAllTags() ([]TagModel, error) { db := common.GetDB() var models []TagModel err := db.Find(&models).Error return models, err } func FindManyArticle(tag, author, limit, offset, favorited string) ([]ArticleModel, int, error) { db := common.GetDB() var models []ArticleModel var count int offset_int, errOffset := strconv.Atoi(offset) if errOffset != nil { offset_int = 0 } limit_int, errLimit := strconv.Atoi(limit) if errLimit != nil { limit_int = 20 } tx := db.Begin() if tag != "" { var tagModel TagModel tx.Where(TagModel{Tag: tag}).First(&tagModel) if tagModel.ID != 0 { // Get article IDs via association var tempModels []ArticleModel if err := tx.Model(&tagModel).Offset(offset_int).Limit(limit_int).Association("ArticleModels").Find(&tempModels); err != nil { tx.Rollback() return models, count, err } count = int(tx.Model(&tagModel).Association("ArticleModels").Count()) // Fetch articles with preloaded associations in single query, ordered by updated_at desc if len(tempModels) > 0 { var ids []uint for _, m := range tempModels { ids = append(ids, m.ID) } tx.Preload("Author.UserModel").Preload("Tags").Where("id IN ?", ids).Order("updated_at desc").Find(&models) } } } else if author != "" { var userModel users.UserModel tx.Where(users.UserModel{Username: author}).First(&userModel) articleUserModel := GetArticleUserModel(userModel) if articleUserModel.ID != 0 { count = int(tx.Model(&articleUserModel).Association("ArticleModels").Count()) // Get article IDs via association var tempModels []ArticleModel if err := tx.Model(&articleUserModel).Offset(offset_int).Limit(limit_int).Association("ArticleModels").Find(&tempModels); err != nil { tx.Rollback() return models, count, err } // Fetch articles with preloaded associations in single query, ordered by updated_at desc if len(tempModels) > 0 { var ids []uint for _, m := range tempModels { ids = append(ids, m.ID) } tx.Preload("Author.UserModel").Preload("Tags").Where("id IN ?", ids).Order("updated_at desc").Find(&models) } } } else if favorited != "" { var userModel users.UserModel tx.Where(users.UserModel{Username: favorited}).First(&userModel) articleUserModel := GetArticleUserModel(userModel) if articleUserModel.ID != 0 { var favoriteModels []FavoriteModel tx.Where(FavoriteModel{ FavoriteByID: articleUserModel.ID, }).Offset(offset_int).Limit(limit_int).Find(&favoriteModels) count = int(tx.Model(&articleUserModel).Association("FavoriteModels").Count()) // Batch fetch articles to avoid N+1 query if len(favoriteModels) > 0 { var ids []uint for _, favorite := range favoriteModels { ids = append(ids, favorite.FavoriteID) } tx.Preload("Author.UserModel").Preload("Tags").Where("id IN ?", ids).Order("updated_at desc").Find(&models) } } } else { var count64 int64 tx.Model(&ArticleModel{}).Count(&count64) count = int(count64) tx.Offset(offset_int).Limit(limit_int).Preload("Author.UserModel").Preload("Tags").Find(&models) } err := tx.Commit().Error return models, count, err } func (self *ArticleUserModel) GetArticleFeed(limit, offset string) ([]ArticleModel, int, error) { db := common.GetDB() models := make([]ArticleModel, 0) var count int offset_int, errOffset := strconv.Atoi(offset) if errOffset != nil { offset_int = 0 } limit_int, errLimit := strconv.Atoi(limit) if errLimit != nil { limit_int = 20 } tx := db.Begin() followings := self.UserModel.GetFollowings() // Batch get ArticleUserModel IDs to avoid N+1 query if len(followings) > 0 { var followingUserIDs []uint for _, following := range followings { followingUserIDs = append(followingUserIDs, following.ID) } var articleUserModels []ArticleUserModel tx.Where("user_model_id IN ?", followingUserIDs).Find(&articleUserModels) var authorIDs []uint for _, aum := range articleUserModels { authorIDs = append(authorIDs, aum.ID) } if len(authorIDs) > 0 { var count64 int64 tx.Model(&ArticleModel{}).Where("author_id IN ?", authorIDs).Count(&count64) count = int(count64) tx.Preload("Author.UserModel").Preload("Tags").Where("author_id IN ?", authorIDs).Order("updated_at desc").Offset(offset_int).Limit(limit_int).Find(&models) } } err := tx.Commit().Error return models, count, err } func (model *ArticleModel) setTags(tags []string) error { if len(tags) == 0 { model.Tags = []TagModel{} return nil } db := common.GetDB() // Batch fetch existing tags var existingTags []TagModel db.Where("tag IN ?", tags).Find(&existingTags) // Create a map for quick lookup existingTagMap := make(map[string]TagModel) for _, t := range existingTags { existingTagMap[t.Tag] = t } // Create missing tags and build final list var tagList []TagModel for _, tag := range tags { if existing, ok := existingTagMap[tag]; ok { tagList = append(tagList, existing) } else { // Create new tag with race condition handling newTag := TagModel{Tag: tag} if err := db.Create(&newTag).Error; err != nil { // If creation failed (e.g., concurrent insert), try to fetch existing var existing TagModel if err2 := db.Where("tag = ?", tag).First(&existing).Error; err2 == nil { tagList = append(tagList, existing) continue } return err } tagList = append(tagList, newTag) } } model.Tags = tagList return nil } func (model *ArticleModel) Update(data interface{}) error { db := common.GetDB() err := db.Model(model).Updates(data).Error return err } func DeleteArticleModel(condition interface{}) error { db := common.GetDB() err := db.Where(condition).Delete(&ArticleModel{}).Error return err } func DeleteCommentModel(condition interface{}) error { db := common.GetDB() err := db.Where(condition).Delete(&CommentModel{}).Error return err }