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

فیلتر کردن لیست‌ها

در این فصل قرار است از پارامترهای query string خود استفاده کنیم تا مشتریان بتوانند فیلم‌ها را بر اساس عنوان یا ژانرهای خاصی جستجو کنند.

به طور دقیق‌تر، یک فیلتر کاهشی می‌سازیم که به مشتریان اجازه می‌دهد بر اساس تطابق دقیق (بدون حساسیت به بزرگی و کوچکی حروف) برای عنوان فیلم و/یا یک یا چند ژانر فیلم جستجو کنند. به عنوان مثال:

// List all movies.
/v1/movies

// List movies where the title is a case-insensitive exact match for 'black panther'.
/v1/movies?title=black+panther

// List movies where the genres include 'adventure'.
/v1/movies?genres=adventure

// List movies where the title is a case-insensitive exact match for 'moana' AND the 
// genres include both 'animation' AND 'adventure'.
/v1/movies?title=moana&genres=animation,adventure

فیلتر کردن پویا در پرس‌وجوی SQL

سخت‌ترین بخش ساختن یک قابلیت فیلتر کردن پویا مانند این، نوشتن پرس‌وجوی SQL برای بازیابی داده‌هاست — باید طوری کار کند که بدون فیلتر، با فیلتر روی هر دو title و genres، یا فقط روی یکی از آنها عمل کند.

برای حل این مشکل، یک گزینه این است که پرس‌وجوی SQL را به صورت پویا در زمان اجرا بسازیم… با اضافه کردن یا درون‌گذاری SQL لازم برای هر فیلتر در عبارت WHERE. اما این رویکرد می‌تواند کد شما را نامرتب و دشوار برای درک کند، به خصوص برای پرس‌وجوهای بزرگی که باید گزینه‌های فیلتر زیادی را پشتیبانی کنند.

در این کتاب یک تکنیک متفاوت را انتخاب می‌کنیم و از یک پرس‌وجوی SQL ثابت استفاده می‌کنیم که به این شکل است:

SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE (LOWER(title) = LOWER($1) OR $1 = '') 
AND (genres @> $2 OR $2 = '{}') 
ORDER BY id

این پرس‌وجوی SQL طوری طراحی شده که هر فیلتر مانند یک گزینه «اختیاری» عمل کند. به عنوان مثال، شرط (LOWER(title) = LOWER($1) OR $1 = '') زمانی true ارزیابی می‌شود که پارامتر جایگذار $1 با عنوان فیلم تطابق دقیق (بدون حساسیت به بزرگی و کوچکی حروف) داشته باشد یا پارامتر جایگذار برابر '' باشد. بنابراین این شرط فیلتر زمانی که عنوان فیلم مورد جستجو رشته خالی "" باشد، در اصل «رد» می‌شود.

شرط (genres @> $2 OR $2 = '{}') به همین شکل کار می‌کند. نماد @> عملگر «شامل بودن» برای آرایه‌های PostgreSQL است و این شرط true برمی‌گرداند اگر هر مقدار در پارامتر جایگذار $2 در فیلد genres پایگاه داده وجود داشته باشد یا پارامتر جایگذار شامل یک آرایه خالی باشد.

یادتان باشد که قبلاً در کتاب، listMoviesHandler خود را طوری تنظیم کردیم که رشته خالی "" و یک برش خالی به عنوان مقادیر پیش‌فرض برای پارامترهای فیلتر title و genres استفاده شوند:

input.Title = app.readString(qs, "title", "")
input.Genres = app.readCSV(qs, "genres", []string{})

پس با کنار هم قرار دادن همه اینها، یعنی اگر مشتری پارامتر title را در query string خود ارائه ندهد، مقدار پارامتر جایگذار $1 رشته خالی "" خواهد بود و شرط فیلتر در پرس‌وجوی SQL به true ارزیابی شده و مانند اینکه «رد» شده عمل می‌کند. همینطور برای پارامتر genres.

خب، بیایید به فایل internal/data/movies.go برگردیم و متد GetAll() را برای استفاده از این پرس‌وجوی جدید به‌روزرسانی کنیم. به این شکل:

فایل: 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 filter conditions.
    query := `
        SELECT id, created_at, title, year, runtime, genres, version
        FROM movies
        WHERE (LOWER(title) = LOWER($1) OR $1 = '') 
        AND (genres @> $2 OR $2 = '{}')     
        ORDER BY id`

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

    // Pass the title and genres as the placeholder parameter values.
    rows, err := m.DB.QueryContext(ctx, query, title, pq.Array(genres))
    if err != nil {
        return nil, err
    }

    defer rows.Close()

    movies := []*Movie{}

    for rows.Next() {
        var movie Movie

        err := rows.Scan(
            &movie.ID,
            &movie.CreatedAt,
            &movie.Title,
            &movie.Year,
            &movie.Runtime,
            pq.Array(&movie.Genres),
            &movie.Version,
        )
        if err != nil {
            return nil, err
        }

        movies = append(movies, &movie)
    }

    if err = rows.Err(); err != nil {
        return nil, err
    }

    return movies, nil
}

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

$ curl "localhost:4000/v1/movies?title=black+panther"
{
    "movies": [
        {
            "id": 2,
            "title": "Black Panther",
            "year": 2018,
            "runtime": "134 mins",
            "genres": [
                "sci-fi",
                "action",
                "adventure"
            ],
            "version": 2
        }
    ]
}
$ curl "localhost:4000/v1/movies?genres=adventure"
{
    "movies": [
        {
            "id": 1,
            "title": "Moana",
            "year": 2015,
            "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
        }
    ]
}
$ curl "localhost:4000/v1/movies?title=moana&genres=animation,adventure"
{
    "movies": [
        {
            "id": 1,
            "title": "Moana",
            "year": 2016,
            "runtime": "107 mins",
            "genres": [
                "animation",
                "adventure"
            ],
            "version": 1
        }
    ]
}

همچنین می‌توانید درخواستی با فیلتری که هیچ رکوردی مطابقت ندارد ارسال کنید. در این حالت، باید یک آرایه JSON خالی در پاسخ دریافت کنید، به این شکل:

$ curl "localhost:4000/v1/movies?genres=western"
{
    "movies": []
}

این خیلی خوب پیش می‌رود. نقطه انتهایی API ما اکنون رکوردهای فیلتر شده مناسب فیلم‌ها را برمی‌گرداند و ما الگویی داریم که به راحتی می‌توانیم آن را برای شامل کردن قوانین فیلتر دیگر در آینده (مانند فیلتر بر اساس سال فیلم یا مدت زمان اجرا) گسترش دهیم.