Доброго дня! Хочу поделиться лайфхаком по реализации универсального способа пагинации в произвольных запроса к БД для получания списков.
Имеем изначально задачу по реализации HTTP REST CRUD сервера на Go. База данных — PostgreSQL. Используемый драйвер — pgx — PostgreSQL Driver and Toolkit.
Задача в общем тривиальная — сделать так, чтоб запросы получали на вход параметры пагинации (номер страницы, количество записей на страницу) и отдавали на выход поля таблицы и общее доступное количество страниц.
Общее количество страниц в таблице БД получить несложно:
SELECT count(*) FROM client_accounts WHERE <условия>
Также, без проблем мы можем выбрать нужную страницу используя LIMIT и OFFSET:
SELECT * FROM client_accounts WHERE <условия> LIMIT a_limit OFFSET a_offset;
Вся соль в том, что крайне нежелательно делать это в лоб двумя запросами к БД. Помимо прочего, чисто теоретически, к моменту выполнения второго запроса данные могут стать неактуальными, если в это время была добавлена/удалена запись в таблицу. Можно, конечно, использовать транзакцию, тем не менее, такой подход показался мне громоздким и не самым удачным. Два запроса — почти всегда хуже, чем один. Кроме того, в запросах секции WHERE должны быть строго одинаковыми.
Не то чтобы задача объединить эти запросы в один сама по себе — мега сложная, тем не менее, сходу придумать или найти готовый пример лично у меня не получилось.
В итоге ,через какое-то время родилось решение, которым и хочу поделиться.
Создаем функцию:
CREATE OR REPLACE FUNCTION get_account_list( IN a_limit INT, IN a_offset INT ) RETURNS TABLE ( list client_accounts, full_count BIGINT ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT a, count(*) OVER() AS full_count FROM client_accounts AS a ORDER BY a.id ASC LIMIT a_limit OFFSET a_offset; END; $$;
Здесь мы возвращаем таблицу с двумя полями — в одном хранится строка таблицы со всеми полями, в другом — количество записей. Это возможно благодаря тому, что всякий раз, когда в PosgreSQL создается таблица, вместе с ней автоматически создается составной тип, представляющий тип строки таблицы, именем которого будет имя таблицы.
Второй нюанс заключается в использовании конструкции OVER(), которая превращает агрегатную функцию COUNT() в оконную и позволяет помещать общее количество записей full_count в каждую строку нашей выборки, в отдельную колонку. В результате выполнения данного запроса нам будет возвращаться таблица с двумя колонками. В первой колонке будут содержаться строки из таблицы client_accounts, во второй — будет повторяться значение full_coun, соответствующее общему количеству строк в выборке.
Чтобы получить все колонки таблицы плюс колонку количества записей, нам нужно “раскрыть” первую колонку в выборке. Поэтому, при вызове функции используем такой запрос:
SELECT (list).*, full_count FROM get_account_list()
Остается демаршалить результат запроса в слайс структур. В данном случае я использовал драйвер pgx и пакет pgxscan:
err = pgxscan.Select(ctx, db.Pool, &data, "SELECT (list).*, full_count FROM get_account_list(10, 0)") if err == nil { for i, v := range data { log.Printf("%d) Name: %s; Login: %s; Password: %s; EMail: %s; Role: %s", i, v.Name, v.Login, v.Password, v.EMail, v.Role) } if len(data) > 0 { log.Println("----------------------------------------") log.Printf("Record count: %d", int(data[0].FullCount)) } else { log.Println("there are no rows in the table") } }
В реальных проектах удобно будет создать некую универсальную процедуру для демаршалинга подобных запросов, принимающую на вход имя функции PostgreSQL, её параметры, limit, offset, и ссылку на слайс структур произвольного типа, в который будут помещены результирующие данные.
Полный код примера доступен на GItHub
ссылка на оригинал статьи https://habr.com/ru/articles/823576/
Добавить комментарий