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  }