Let's Go Further فیلتر کردن، مرتب‌سازی و صفحه‌بندی › صفحه‌بندی لیست‌ها
قبلی · فهرست مطالب · بعدی
فصل ۹.۷.

صفحه‌بندی لیست‌ها

اگر endpointای دارید که لیستی با صدها یا هزاران رکورد برمی‌گرداند، ممکن است به دلایل عملکرد یا قابلیت استفاده، بخواهید نوعی صفحه‌بندی را در endpoint پیاده‌سازی کنید — به طوری که در هر پاسخ HTTP فقط زیرمجموعه‌ای از رکوردها برگردانده شود.

برای نمایش نحوه انجام این کار، در این فصل endpoint GET /v1/movies را به‌روزرسانی می‌کنیم تا از مفهوم ‘صفحات’ پشتیبانی کند و client بتواند با استفاده از پارامترهای query string ما به نام‌های page و page_size، ‘صفحه’ خاصی از لیست فیلم‌ها را درخواست کند. به عنوان مثال:

// Return the 5 records on page 1 (records 1-5 in the dataset)
/v1/movies?page=1&page_size=5

// Return the next 5 records on page 2 (records 6-10 in the dataset)
/v1/movies?page=2&page_size=5

// Return the next 5 records on page 3 (records 11-15 in the dataset)
/v1/movies?page=3&page_size=5

به طور کلی، تغییر پارامتر page_size تعداد فیلم‌های نمایش داده شده در هر ‘صفحه’ را تغییر می‌دهد و افزایش پارامتر page به مقدار یک، ‘صفحه’ بعدی فیلم‌ها در لیست را نمایش می‌دهد.

عبارت‌های LIMIT و OFFSET

در پشت صحنه، ساده‌ترین راه برای پشتیبانی از این سبک صفحه‌بندی، اضافه کردن عبارت‌های LIMIT و OFFSET به query SQL ما است.

عبارت LIMIT به شما اجازه می‌دهد حداکثر تعداد رکوردهایی که یک query SQL باید برگرداند را تنظیم کنید و OFFSET به شما اجازه می‌دهد تعداد مشخصی از ردیف‌ها را قبل از شروع برگرداندن رکوردها از query ‘رد کنید’.

در برنامه ما، فقط کافی است مقادیر page و page_size ارائه شده توسط client را به مقادیر LIMIT و OFFSET مناسب برای query SQL خود تبدیل کنیم. محاسبات بسیار ساده است:

LIMIT = page_size
OFFSET = (page - 1) * page_size

یا برای مثال مشخص، اگر client درخواست زیر را ارسال کند:

/v1/movies?page_size=5&page=3

باید این را به query SQL زیر ‘ترجمه’ کنیم:

SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '') 
AND (genres @> $2 OR $2 = '{}')     
ORDER BY %s %s, id ASC
LIMIT 5 OFFSET 10

بیایید با اضافه کردن چند متد کمکی به ساختار Filters خود برای محاسبه مقادیر LIMIT و OFFSET مناسب شروع کنیم.

اگر در حال دنبال کردن هستید، فایل internal/data/filters.go را به این صورت به‌روزرسانی کنید:

فایل: internal/data/filters.go
package data

...

type Filters struct {
    Page         int
    PageSize     int
    Sort         string
    SortSafelist []string
}

...

func (f Filters) limit() int {
    return f.PageSize
}

func (f Filters) offset() int {
    return (f.Page - 1) * f.PageSize
}

...

به‌روزرسانی مدل پایگاه داده

به عنوان مرحله نهایی در این فرآیند، باید متد GetAll() مدل پایگاه داده خود را به‌روزرسانی کنیم تا عبارت‌های LIMIT و OFFSET مناسب به query SQL اضافه شوند.

فایل: internal/data/movies.go
package data

...

func (m MovieModel) GetAll(title string, genres []string, filters Filters) ([]*Movie, error) {
    // Update the SQL query to include the LIMIT and OFFSET clauses with placeholder
    // parameter values.
    query := fmt.Sprintf(`
        SELECT id, created_at, title, year, runtime, genres, version
        FROM movies
        WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '') 
        AND (genres @> $2 OR $2 = '{}')     
        ORDER BY %s %s, id ASC
        LIMIT $3 OFFSET $4`, filters.sortColumn(), filters.sortDirection())

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    // As our SQL query now has quite a few placeholder parameters, let's collect the
    // values for the placeholders in a slice. Notice here how we call the limit() and
    // offset() methods on the Filters struct to get the appropriate values for the
    // LIMIT and OFFSET clauses.
    args := []any{title, pq.Array(genres), filters.limit(), filters.offset()}

    // And then pass the args slice to QueryContext() as a variadic parameter.
    rows, err := m.DB.QueryContext(ctx, query, args...)
    if err != nil {
        return nil, err
    }

    // The remaining code doesn't need to change.
    ...
}

پس از اتمام این کار، باید آماده آزمایش باشیم.

سرور را مجدداً راه‌اندازی کنید و سپس درخواست زیر را با پارامتر page_size=2 ارسال کنید:

$ curl "localhost:4000/v1/movies?page_size=2"
{
    "movies": [
        {
            "id": 1,
            "title": "Moana",
            "year": 2016,
            "runtime": "107 mins",
            "genres": [
                "animation",
                "adventure"
            ],
            "version": 1
        },
        {
            "id": 2,
            "title": "Black Panther",
            "year": 2018,
            "runtime": "134 mins",
            "genres": [
                "sci-fi",
                "action",
                "adventure"
            ],
            "version": 2
        }
    ]
}

این خوب به نظر می‌رسد. اکنون endpoint ما فقط دو رکورد اول فیلم را از پایگاه داده ما برمی‌گرداند (با استفاده از ترتیب پیش‌فرض مرتب‌سازی صعودی شناسه فیلم).

سپس، درخواست صفحه دوم نتایج را امتحان کنید. اگر در حال دنبال کردن بوده‌اید، این صفحه باید شامل یک رکورد باقی‌مانده در سیستم ما باشد، به این صورت:

# مهم: این URL باید با علامت نقل قول دوگانه احاطه شود تا به درستی کار کند.
$ curl "localhost:4000/v1/movies?page_size=2&page=2"
{
    "movies": [
        {
            "id": 4,
            "title": "The Breakfast Club",
            "year": 1985,
            "runtime": "97 mins",
            "genres": [
                "comedy"
            ],
            "version": 5
        }
    ]
}

اگر سعی کنید صفحه سوم را درخواست کنید، باید آرایه JSON خالی در پاسخ دریافت کنید به این صورت:

$ curl "localhost:4000/v1/movies?page_size=2&page=3"
{
    "movies": []
}