goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/database/paginator.go (about) 1 package database 2 3 import ( 4 "math" 5 6 "gorm.io/gorm" 7 "gorm.io/gorm/clause" 8 "goyave.dev/goyave/v5/util/errors" 9 ) 10 11 // Paginator structure containing pagination information and result records. 12 type Paginator[T any] struct { 13 DB *gorm.DB `json:"-"` 14 15 Records *[]T `json:"records"` 16 17 rawQuery string 18 rawQueryVars []any 19 rawCountQuery string 20 rawCountQueryVars []any 21 22 MaxPage int64 `json:"maxPage"` 23 Total int64 `json:"total"` 24 PageSize int `json:"pageSize"` 25 CurrentPage int `json:"currentPage"` 26 27 loadedPageInfo bool 28 } 29 30 // PaginatorDTO structure sent to clients as a response. 31 type PaginatorDTO[T any] struct { 32 Records []T `json:"records"` 33 MaxPage int64 `json:"maxPage"` 34 Total int64 `json:"total"` 35 PageSize int `json:"pageSize"` 36 CurrentPage int `json:"currentPage"` 37 } 38 39 func paginateScope(page, pageSize int) func(db *gorm.DB) *gorm.DB { 40 return func(db *gorm.DB) *gorm.DB { 41 offset := (page - 1) * pageSize 42 return db.Offset(offset).Limit(pageSize) 43 } 44 } 45 46 // NewPaginator create a new Paginator. 47 // 48 // Given DB transaction can contain clauses already, such as WHERE, if you want to 49 // filter results. 50 // 51 // articles := []model.Article{} 52 // tx := db.Where("title LIKE ?", "%"+sqlutil.EscapeLike(search)+"%") 53 // paginator := database.NewPaginator(tx, page, pageSize, &articles) 54 // err := paginator.Find() 55 // if response.WriteDBError(err) { 56 // return 57 // } 58 // response.JSON(http.StatusOK, paginator) 59 func NewPaginator[T any](db *gorm.DB, page, pageSize int, dest *[]T) *Paginator[T] { 60 return &Paginator[T]{ 61 DB: db, 62 CurrentPage: page, 63 PageSize: pageSize, 64 Records: dest, 65 } 66 } 67 68 // Raw set a raw SQL query and count query. 69 // The Paginator will execute the raw queries instead of automatically creating them. 70 // The raw query should not contain the "LIMIT" and "OFFSET" clauses, they will be added automatically. 71 // The count query should return a single number (`COUNT(*)` for example). 72 func (p *Paginator[T]) Raw(query string, vars []any, countQuery string, countVars []any) *Paginator[T] { 73 p.rawQuery = query 74 p.rawQueryVars = vars 75 p.rawCountQuery = countQuery 76 p.rawCountQueryVars = countVars 77 return p 78 } 79 80 func (p *Paginator[T]) updatePageInfo(db *gorm.DB) error { 81 count := int64(0) 82 db = db.Session(&gorm.Session{Initialized: true}) 83 if len(db.Statement.Preloads) > 0 { 84 db.Statement.Preloads = map[string][]any{} 85 } 86 if len(db.Statement.Selects) > 0 { 87 db.Statement.Selects = []string{} 88 } 89 90 var res *gorm.DB 91 if p.rawCountQuery != "" { 92 res = db.Raw(p.rawCountQuery, p.rawCountQueryVars...).Scan(&count) 93 } else { 94 res = db.Model(p.Records).Count(&count) 95 } 96 if res.Error != nil { 97 return errors.New(res.Error) 98 } 99 p.Total = count 100 p.MaxPage = int64(math.Ceil(float64(count) / float64(p.PageSize))) 101 if p.MaxPage == 0 { 102 p.MaxPage = 1 103 } 104 p.loadedPageInfo = true 105 return nil 106 } 107 108 // UpdatePageInfo executes count request to calculate the `Total` and `MaxPage`. 109 // When calling this function manually, it is advised to use a transaction that is calling 110 // `Find()` too, to avoid inconsistencies. 111 func (p *Paginator[T]) UpdatePageInfo() error { 112 return p.updatePageInfo(p.DB) 113 } 114 115 // Find requests page information (total records and max page) if not already fetched using 116 // `UpdatePageInfo()` and executes the query. The `Paginator` struct is updated automatically, 117 // as well as the destination slice given in `NewPaginator()`. 118 // 119 // The two queries are executed inside a transaction. 120 func (p *Paginator[T]) Find() error { 121 return p.DB.Session(&gorm.Session{}).Transaction(func(tx *gorm.DB) error { 122 if !p.loadedPageInfo { 123 err := p.updatePageInfo(tx) 124 if err != nil { 125 return errors.New(err) 126 } 127 } 128 129 if p.rawQuery != "" { 130 p.DB = p.rawStatement(tx).Scan(p.Records) 131 } else { 132 p.DB = tx.Scopes(paginateScope(p.CurrentPage, p.PageSize)).Find(p.Records) 133 } 134 if p.DB.Error != nil { 135 p.loadedPageInfo = false // Invalidate previous page info. 136 return errors.New(p.DB.Error) 137 } 138 return nil 139 }) 140 } 141 142 func (p *Paginator[T]) rawStatement(tx *gorm.DB) *gorm.DB { 143 offset := (p.CurrentPage - 1) * p.PageSize 144 rawStatement := tx.Raw(p.rawQuery, p.rawQueryVars...) 145 pageSize := p.PageSize 146 rawStatement.Statement.SQL.WriteString(" ") 147 clause.Limit{Limit: &pageSize, Offset: offset}.Build(rawStatement.Statement) 148 return rawStatement 149 }