JavaRush /Курси /Go SELF /Параметри запиту — фільтри, сортування, пагінація

Параметри запиту — фільтри, сортування, пагінація

Go SELF
Рівень 57 , Лекція 1
Відкрита

1. Параметри запиту як частина контракту

Коли ви вперше бачите /api/v1/tasks?done=true&limit=20, легко подумати: «Та це ж просто рядок після ?, яка тут різниця». Різниця величезна: параметри запиту — це спосіб налаштувати видачу, не змінюючи сам ресурс. Тобто ресурс лишається «списком задач», але ми кажемо серверу, який саме список нам потрібен і як його показати.

У ресурсу списку задач /api/v1/tasks майже завжди є три природні «потреби клієнта».

Перша — відфільтрувати список: показати лише виконані або лише невиконані, знайти за підрядком, вибрати задачі з певними тегами. Друга — відсортувати: за id або за назвою, щоб не дивуватися, чому сьогодні порядок один, а завтра інший. Третя — обмежити обсяг: отримати 20 задач замість 2000, тобто застосувати пагінацію.

Важливо зафіксувати правильну філософію: параметри запиту керують представленням списку, але не замінюють ідентифікацію ресурсу. Якщо потрібно обов’язково вказати, «яку саме задачу», то це зазвичай шлях (/api/v1/tasks/{id}), а не query. Query — це «налаштування видачі».

2. Словник параметрів для /api/v1/tasks

Коли починаєш додавати параметри по одному, легко отримати хаос: сьогодні done, завтра isDone, післязавтра completed, і клієнту доводиться мучитися з копіпастою. Тому ми робимо дорослу річ: фіксуємо набір параметрів як словник контракту для лістингу задач. Він має бути коротким, стабільним і не намагатися «впхати весь SQL в один рядок».

Нижче — базовий контракт, достатній для навчального проєкту й майже для будь-якого простого реального API. Ми свідомо тримаємо його мінімальним: що менше параметрів, то простіша підтримка і менше сюрпризів.

Параметр Тип (після парсингу) Що означає Значення за замовчуванням (якщо параметр не передано) Приклад
done
bool
(але параметр необов’язковий)
Фільтр за статусом виконання фільтр не застосовується
done=true
q
string
Пошук за текстом (наприклад, у полі title) порожній пошук (без фільтрації)
q=milk
tag
[]string
(повторюваний ключ)
Фільтр за тегами фільтрація за тегами не застосовується
tag=go&tag=http
sort
enum (
id
,
title
)
За яким полем сортуємо
id
sort=title
order
enum (
asc
,
desc
)
Напрямок сортування
asc
order=desc
limit
int
Скільки елементів повернути
20
limit=50
offset
int
З якого елемента почати
0
offset=100

Є два важливі змістові застереження.

По-перше, done — необов’язковий фільтр. Якщо done не задано, це не означає done=false; це означає «покажи все». Ми ще повернемося до цієї думки, бо це джерело класичних багів.

По-друге, tag — повторюваний параметр, і це нормальний спосіб передати список у запиті. Не саморобний CSV в одному рядку, а саме tag=go&tag=http.

3. Нюанси проєктування параметрів

Найменування параметрів

У назвах API-параметрів найчастіше помиляються не компілятори, а люди. Параметр назвали один раз, клієнт вшив його в мобільний застосунок, і раптом виявилося, що перейменувати його «безболісно» вже не можна. Тому до неймінгу параметрів запиту варто ставитися як до публічного інтерфейсу: нудно, акуратно, передбачувано.

Тут працює просте правило: ім’я параметра має бути коротким, зрозумілим і без фантазій. Ми використовуємо нижній регістр і прості слова: done, q, tag, sort, order, limit, offset. Це не самовираження, а контракт.

Окремо про q. Іноді в новачків виникає думка: «А чому не query?» Можна і query, але короткий q — звичний патерн для пошукових форм і багатьох API. Головне — обрати один варіант і дотримуватися його.

Про багатослівні імена. Якщо вони все-таки потрібні, краще дотримуватися snake_case (наприклад, created_at, due_before), але в поточній версії контракту ми свідомо тримаємо лише однослівні параметри, щоб не ускладнювати.

Фільтри

Фільтри — це момент, коли користувач каже: «Мені потрібен не весь список задач, а підмножина». І тут легко випадково зробити API, яке начебто працює, але його неможливо пояснити й протестувати. Тому фільтри мають бути простими та комбінованими.

Найпростіша й найзрозуміліша модель комбінування — усі фільтри застосовуються одночасно, тобто логічне AND.

Наприклад, запит:

/api/v1/tasks?done=false&q=milk&tag=home&tag=shopping

розумно читати так: «Дай мені невиконані задачі, де текст містить milk, і які мають теги home та shopping». Можна сперечатися, чи мають теги означати AND чи OR, і саме це — частина контракту, яку важливо зафіксувати словами.

Для навчального проєкту зручно прийняти правило: якщо передано кілька tag, то задача підходить, якщо містить принаймні один із тегів (АБО всередині тегів), а щодо інших фільтрів усе одно І. Виходить: «фільтр done І пошук І (tag1 АБО tag2 АБО ...)». Це легше пояснити користувачу і простіше реалізувати.

У межах цієї лекції ми не пишемо код фільтрації, але вже проєктуємо поведінку так, щоб вона була передбачуваною.

Сортування

Сортування здається простою справою, поки ви не зрозумієте, що «за замовчуванням» — це теж сортування, тільки неявне. А неявне сортування зазвичай означає «як пощастить»: сьогодні база віддала в одному порядку, завтра — в іншому, а тести клієнта починають поводитися нестабільно.

Тому для списку задач має бути визначений детермінований порядок за замовчуванням. У нашому контракті це sort=id і order=asc. Чому id? Тому що це найстабільніше й однозначне поле в базовому CRUD.

Також корисно подумки домовитися про tie-breaker, тобто вторинний ключ сортування. Навіть якщо ми сортуємо за title, у двох задач може бути однакова назва. Тоді порядок усе одно має бути стабільним — зазвичай другим ключем стає id. Це деталь реалізації, але її корисно тримати в голові: інакше сортування за title буде стрибати, і користувач думатиме, що API живе власним життям.

Параметри sort і order проєктуються парою: sort відповідає на запитання «за чим», order — «у якому напрямку». Це простіше, ніж намагатися втиснути в один параметр щось на кшталт sort=title_desc.

Пагінація

Пагінація — це не лише зручність інтерфейсу, а й захист сервера та клієнта від випадкового «вивантажте мені всі задачі за весь час». Якщо хтось колись робив SELECT * без LIMIT по таблиці на мільйон рядків, він потім починає поважати пагінацію майже релігійно, і це нормально.

Ми використовуємо просту модель limit/offset. Читається вона так: offset — скільки пропустити з початку відсортованого списку, limit — скільки повернути після цього.

Наприклад: /api/v1/tasks?sort=id&order=asc&limit=20&offset=40

означає: «Поверни задачі з 41-ї по 60-ту в порядку зростання id». І одразу важлива думка: пагінація має сенс лише разом із фіксованим сортуванням. Інакше «сторінки» можуть перемішуватися.

Контрактом потрібно зафіксувати розумні обмеження. Наприклад, limit за замовчуванням 20, а максимальний limit100. Це не примха викладача, а нормальна практика: якщо клієнту раптом потрібно вивантажити 5000 задач, це окремий сценарій, і його треба робити свідомо.

Повторювані параметри

Повторювані параметри запиту інколи виглядають дивно для тих, хто звик передавати масиви в JSON. Але в URL це природний патерн: один ключ може мати кілька значень, бо query string за своєю природою ближчий до map[string][]string, а не до map[string]string.

Тобто tag=go&tag=http — це не хак і не костиль, а стандартний спосіб виразити список. Ви побачите його в найрізноманітніших системах: від простих REST API до складних пошукових запитів.

З практичного боку це дає приємну властивість: якщо клієнт хоче додати тег, він не повинен парсити CSV-рядок, акуратно екранувати коми й думати, що робити з тегом dev,ops. Він просто додає ще один tag.

У стандартній бібліотеці Go це саме так і моделюється: під час розбору query ви отримуєте структуру, де ключ може мати кілька значень, а код потім бере або перше значення, або всі одразу. Навіть у прикладах, де з query беруть одне значення через r.URL.Query().Get("guess"), мається на увазі, що query — це набір параметрів, який сервер читає структуровано, а не шляхом розбиття за символом &.

4. Приклади запитів до /api/v1/tasks

Коли контракт зафіксовано, стає простіше «думати URL-ами». Це корисна навичка: ви дивитеся на посилання і розумієте, що сервер має зробити, без ворожіння та магії.

Ось кілька прикладів і те, як їх правильно інтерпретувати.

package main

import "fmt"

func main() {
	fmt.Println("/api/v1/tasks") // список задач без фільтрів; sort=id, order=asc, limit=20, offset=0
}

Тут узагалі немає query. І це нормально: значення за замовчуванням — частина контракту, і відсутність параметрів має мати чітке значення.

package main

import "fmt"

func main() {
	fmt.Println("/api/v1/tasks?done=true") // лише виконані, решта параметрів за замовчуванням
}

done=true застосовує фільтр, але не змінює сортування чи пагінацію.

package main

import "fmt"

func main() {
	fmt.Println("/api/v1/tasks?q=milk&tag=home&tag=shopping") // пошук і фільтр за тегами
}

Ми читаємо це так: «пошук за milk і фільтр за тегами». Як саме трактувати кілька тегів — АБО чи І — ми заздалегідь фіксуємо в контракті, а не як вийде.

package main

import "fmt"

func main() {
	fmt.Println("/api/v1/tasks?sort=title&order=desc&limit=10&offset=20") // сортування й пагінація
}

Тут запит уже схожий на повноцінний клієнтський лістинг: відсортували за назвою за спаданням і взяли сторінку.

5. Модель параметрів у коді

Тут хочеться одразу перейти до парсингу limit через strconv.Atoi і валідації sort через список дозволених значень. Але за нашим планом дня це буде наступними кроками. Зараз ми робимо рівно одну корисну річ: проєктуємо тип у коді, який зберігає типізовані параметри лістингу.

Це допомагає не змішувати читання параметрів і бізнес-логіку списку задач. Ми хочемо прийти до схеми: спочатку розпарсили й провалідовували вхід, отримали структуру параметрів, і лише потім передали її у функцію, яка вміє відбирати задачі.

package main

type TaskListQuery struct {
	// Done: nil = фільтр не передано; true/false = фільтр передано.
	Done *bool

	Q    string   // рядок пошуку
	Tags []string // повторювані tag=...

	Sort  string // "id" або "title"
	Order string // "asc" або "desc"

	Limit  int
	Offset int
}

Зверніть увагу на *bool. Це не любов Go до вказівників, а спосіб чесно зберігати три стани: «не передано», «true», «false». Якщо зробити просто bool, ви не відрізните done=false від «done не надіслали». А це вже інший зміст.

Тепер задамо значення за замовчуванням. Ними зручніше керувати централізовано, а не розкидати 20 і id по коду, як конфеті.

package main

func DefaultTaskListQuery() TaskListQuery {
	return TaskListQuery{
		Done:   nil,
		Q:      "",
		Tags:   nil,
		Sort:   "id",
		Order:  "asc",
		Limit:  20,
		Offset: 0,
	}
}

І тепер у нас з’являється чітка межа: парсер параметрів запиту має взяти DefaultTaskListQuery(), застосувати те, що прийшло від клієнта, і або повернути готову структуру, або помилку валідації. Саму реалізацію парсингу ми робитимемо пізніше, і там якраз з’являться url.Values, Get, списки значень тощо.

До речі, у Go стандартна бібліотека показує, що «розбір query → отримання рядка → конвертація → валідація» — це абсолютно нормальний шлях: спочатку ви берете рядкове значення з query, потім перетворюєте його на число й перевіряєте межі. А якщо потрібно розібрати query як окремий рядок, без повного URL, для цього теж є стандартний інструмент: розбір query-рядка з подальшим Encode() трапляється навіть у прикладах тестування.

6. Типові помилки під час проєктування параметрів запиту

Помилка № 1: робити обов’язкові параметри в query.
Якщо клієнт зобов’язаний передати id як ?id=123, ви майже напевно ускладнюєте життя: це вже ідентифікація ресурсу, а не налаштування видачі. Ідентифікація живе в шляху, а query залишають для фільтрів, сортування й пагінації.

Помилка № 2: не фіксувати значення за замовчуванням і «сподіватися на здоровий глузд».
Без значень за замовчуванням контракт перетворюється на ворожіння: клієнт не знає, скільки елементів повернеться, у якому порядку й що означає відсутність параметра. Значення за замовчуванням — це не дрібниця, а частина публічної поведінки API.

Помилка № 3: плутати «параметр відсутній» і «параметр дорівнює false/0/порожньо».
Класика — done=false проти відсутнього done. Якщо ви зберігаєте done як bool, у вас неминуче з’явиться логіка: а як зрозуміти, чи його передали. У підсумку код починає вигадувати евристики, і баги з’являються раптово.

Помилка № 4: приймати будь-які значення sort і «розрулювати потім».
Якщо сервер мовчки приймає sort=banana, а потім або падає, або сортує якось за замовчуванням, клієнт не розуміє, що зробив не так. Для переліків потрібен список дозволених значень — інакше контракт розпливається.

Помилка № 5: не обмежувати limit і не думати про навантаження.
limit=1000000 — це не «користувач захотів», а потенційна атака або, принаймні, випадкове перевантаження. Навіть у навчальному проєкті корисно мислити як інженер: ставити розумні межі й пояснювати їх помилкою валідації.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ