- Что такое Knex и PostgreSQL?
- Почему стоит использовать Knex?
- Предварительные условия
- Создание сервера Node.js
- Настройка Knex
- Создание файла миграции
- Создание отношений между таблицами
- Создание службы
- Создание контроллера
- Создание маршрутов API
- Тестирование API
- Конечная точка пользователя
- Конечная точка блога
- Просмотр данных пользователя с помощью Arctype
- Заключение
Что такое Knex и PostgreSQL?
Knex — это универсальный, портативный и удобный конструктор SQL-запросов для PostgreSQL, CockroachDB, MSSQL, MySQL, MariaDB, SQLite3, Better-SQLite3, Oracle и Amazon Redshift. PostgreSQL — это объектно-реляционная система управления базами данных с открытым исходным кодом, обладающая высокой степенью гибкости. Она способна работать с широким спектром вариантов использования, включая отдельные машины, хранилища данных и веб-сервисы с множеством одновременных пользователей. Это реляционная система управления базами данных, которая использует и расширяет SQL (отсюда и название), и она широко расширяема для различных случаев использования, помимо транзакционных данных.
PostgreSQL хранит информацию в таблицах (называемых отношениями), которые содержат кортежи, представляющие сущности (например, документы и людей) и отношения (например, авторство). Атрибуты фиксированного типа, представляющие свойства сущности (например, название), а также первичный ключ, включены в отношения. Типы атрибутов могут быть атомарными (например, целочисленными, с плавающей точкой или булевыми) или структурированными (например, массив, вложенный JSON или процедура).
Почему стоит использовать Knex?
Большинство разработчиков используют Knex в качестве конструктора запросов по следующим причинам.
- Он позволяет им создавать запросы, как будто они пишут код на Javascript, в то время как он обрабатывает перевод в SQL.
- Он поддерживает такие системы управления базами данных, как PostgreSQL, MySQL, SQLite и Oracle.
- Он поддерживает как традиционные обратные вызовы в стиле node, так и интерфейс promise для более чистого управления потоком async, а также интерфейс потоков.
- Это полнофункциональные конструкторы запросов и схем, поддержка транзакций (с точками сохранения), пул соединений и стандартизированные ответы между клиентами запросов и диалектами.
Предварительные условия
Поскольку это практический демонстрационный учебник, для начала работы убедитесь, что ваши системы отвечают следующим требованиям:
- У вас установлен Node.js версии 14 или более поздней.
- Вы установили и настроили Arctype
- Настроена база данных PostgreSQL
- Установите Knex CLI (команда для этого
npm i -g knex
).
Выполнив все вышеперечисленные требования, давайте создадим новую базу данных с помощью Arctype. Для начала запустите клиент Arctype, затем выберите базу данных, с которой вы хотите работать:
Затем укажите учетные данные базы данных. Все очень просто, никаких хлопот!
Если у вас уже настроена база данных, вы всегда можете создать новую, добавив новый источник данных:
После того как вы закончите, вы должны увидеть таблицы под вашей базой данных с левой стороны в Arctype.
Создание сервера Node.js
Теперь создайте новую папку для вашего проекта и инициализируйте новый проект с помощью команд, приведенных ниже.
mkdir knex-demo && cd knex-demo
npm init -y
Затем установите необходимые пакеты, выполнив следующую команду:
npm install pg express knex
С помощью приведенной выше команды вы установили модуль PostgreSQL Node.js, express
и модуль knex
.
Теперь создайте следующую структуру папок в папке knex-demo.
Затем в файле app.js
создайте сервер Node.js Express с помощью приведенного ниже фрагмента кода.
const express = require("express");
const app = express();
app.use(express.json());
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
Наконец, измените файл package.json
, чтобы добавить команду сценария.
"scripts": {
"start": "node src/app.js"
},
Настройка Knex
После того как вы создали базу данных, давайте настроим Knex и подключимся к базе данных. Чтобы начать работу, выполните приведенную ниже команду в терминале для инициализации knex.
knex init
Приведенная выше команда создаст файл knexfile.js
в корневом каталоге вашего проекта с фрагментами кода для подключения базы данных к различным средам (development, staging, production.) По умолчанию в среде разработки используется база данных SQLite, вам нужно будет изменить код для использования базы данных Postgres.
// Update with your config settings.
/**
* @type { Object.<string, import("knex").Knex.Config> }
*/
module.exports = {
development: {
client: "postgresql",
connection: {
database: "blogs",
user: "postgres",
password: "1234",
},
},
staging: {
client: "postgresql",
connection: {
database: "<Your Staging DB>",
user: "username",
password: "password",
},
pool: {
min: 2,
max: 10,
},
migrations: {
tableName: "knex_migrations",
},
},
production: {
client: "postgresql",
connection: {
database: "<Your Production DB>",
user: "username",
password: "password",
},
pool: {
min: 2,
max: 10,
},
migrations: {
tableName: "knex_migrations",
},
},
};
Ваш файл knexfile.js
должен выглядеть как приведенный выше фрагмент кода. Вы можете изменить код, чтобы он соответствовал любым другим требованиям проекта.
Создание файла миграции
Теперь выполните команду ниже, чтобы создать файл миграции для таблицы пользователя, и определите, как будет выглядеть таблица, выполнив команду ниже.
knex migrate:make users
Приведенная выше команда создаст файл migrations/timestamp_users
в корневом каталоге вашего проекта. Теперь давайте определим схему в функциях подъема и спуска.
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable("users", (table) => {
table.increments("id").primary();
table.string("name").notNullable();
table.string("email").notNullable();
table.string("password").notNullable();
table.string("avatar").defaultTo("https://i.imgur.com/Xq2bZCY.png");
table.string("bio").defaultTo("I am a new user");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTable("users");
};
Код в файле migrations/timestamp_users.js
должен выглядеть как приведенный выше фрагмент кода. Мы определили схему пользователя. Первое — это поле id
с автоинкрементом, установленным на true, и уникальным ограничением, после чего у нас есть поля, необходимые для таблицы пользователя.
Затем в функции down
мы удаляем все существующие таблицы с именем users перед созданием нашей новой таблицы.
Чтобы создать эту таблицу в базе данных, нужно снова выполнить команду migrations, на этот раз добавив флаг latest
, чтобы зафиксировать только новые изменения в файле.
knex migrate:latest
Далее создайте схему блогов, выполнив в терминале приведенную ниже команду migration.
knex migrate:make blogs
Затем добавьте приведенный ниже код в функцию up, чтобы определить схему блога.
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable("blog", (table) => {
table.increments("id").primary();
table.string("title").notNullable();
table.string("content").notNullable();
table.string("image").notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTable("blog");
};
В приведенном выше фрагменте кода мы создали схему блога и определили необходимые нам поля в таблице blogs. Мы также отбрасываем все существующие таблицы с именем blogs.
Создание отношений между таблицами
Теперь давайте создадим связь между схемой пользователя и схемой блога. Таким образом, мы сможем связать блоги с пользователями, которые их создают. Для этого нам нужно обновить код в файле timestamps_blogs.js
следующим кодом:
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable("blog", (table) => {
table.increments("id").primary();
table.string("title").notNullable();
table.string("content").notNullable();
table.string("image").notNullable();
table
.integer("author")
.unsigned()
.references("id")
.inTable("users")
.onDelete("CASCADE");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTable("blog");
};
В приведенном выше фрагменте кода мы изменили поле author, чтобы оно ссылалось на id каждого пользователя в схеме пользователя. Функция OnDelete
обеспечит удаление блога после удаления учетной записи пользователя.
Далее создайте файл db.js
в папке config и добавьте фрагмент кода, приведенный ниже.
const knex = require("knex");
const config = require("../../knexfile");
module.exports = knex(config.development);
В приведенном выше фрагменте кода мы импортируем config
из knexfile
и инициализируем объект knex, поэтому, поскольку приложение запускается в среде разработки, мы будем вызывать объект development
config.
Создание службы
Создав таблицы базы данных, давайте создадим службу, которая будет выполнять операции CRUD в таблицах базы данных. Создайте файл userService.js
в папке service
и добавьте фрагмент кода, указанный ниже.
const db = require('../config/db');
module.exports = userService = {
getAll: async () => {
const users = await db("users");
return users;
},
getById: async (id) => {
const user = await db("users").where("id", id);
return user;
},
create: async (user) => {
const users = await db("users").insert(user);
return users;
},
update: async (id, user) => {
const users = await db("users").where("id", id).update({
name: user.name,
email: user.email,
password: user.password,
avatar: user.avatar,
bio: user.bio,
});
return users;
},
delete: async (id) => {
const users = await db("users").where("id", id).del();
return users;
},
};
В приведенном выше фрагменте кода мы импортировали конфиг knex. Затем мы создали объект userService
и создали методы для операций CRUD.
Далее создайте файл blogService.js
в папке с сервисом и добавьте приведенный ниже фрагмент кода.
const db = require("../config/db");
module.exports = blogService = {
getAll: async () => {
const blogs = await db("blog")
.join("users", "users.id", "blog.author")
.select(
"blog.*",
"users.name",
"users.avatar",
"users.bio",
"users.email"
);
return blogs;
},
getById: async (id) => {
console.log(id);
const blog = await db("blog").where({ id });
return blog;
},
create: async (blog) => {
const blogs = await db("blog").insert(blog);
return blogs;
},
update: async (id, blog) => {
const blogs = await db("blog").where("id", id).update({
title: blog.title,
content: blog.content,
image: blog.image,
});
return blogs;
},
delete: async (id) => {
const blogs = await db("blogs").where("id", id).del();
return blogs;
},
};
В приведенном выше фрагменте кода мы создали CRUD-операции для сервиса blogService. В методе getAll
мы соединяем таблицу users
с таблицей blogs
, используя метод select для select
полей, которые мы хотим показать пользователям — если мы теперь вызовем сервис, мы сможем получить блоги и пользователей, которые их опубликовали.
Создание контроллера
Теперь давайте создадим контроллер для использования сервиса, который мы только что создали. Начнем с контроллера пользователя. Создайте файл userController.js
в папке controller и добавьте в него фрагмент кода, приведенный ниже.
const userService = require("../service/userService");
module.exports = userController = {
getAll: async (req, res, next) => {
try {
const users = await userService.getAll();
res.json(users);
} catch (error) {
next(error);
}
},
getById: async (req, res, next) => {
try {
const user = await userService.getById(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
},
create: async (req, res, next) => {
try {
const user = await userService.create(req.body);
res.json(user);
} catch (error) {
next(error);
}
},
update: async (req, res, next) => {
try {
const user = await userService.update(req.params.id, req.body);
res.json(user);
} catch (error) {
next(error);
}
},
delete: async (req, res, next) => {
try {
const user = await userService.delete(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
},
};
Теперь создайте файл blogController.js
в папке controller для потребления blogService
с помощью приведенного ниже фрагмента кода.
const userService = require("../service/userService");
module.exports = userController = {
getAll: async (req, res, next) => {
try {
const users = await userService.getAll();
res.json(users);
} catch (error) {
next(error);
}
},
getById: async (req, res, next) => {
try {
const user = await userService.getById(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
},
create: async (req, res, next) => {
try {
const user = await userService.create(req.body);
res.json(user);
} catch (error) {
next(error);
}
},
update: async (req, res, next) => {
try {
const user = await userService.update(req.params.id, req.body);
res.json(user);
} catch (error) {
next(error);
}
},
delete: async (req, res, next) => {
try {
const user = await userService.delete(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
},
};
Создание маршрутов API
Далее давайте создадим API-маршруты для контроллеров. Для начала создайте файл user.js
в папке routes
и добавьте фрагмент кода, приведенный ниже.
const express = require("express");
const router = express.Router();
const userController = require("../controller/userController");
/* GET users listing. */
router.route("/").get(userController.getAll).post(userController.create);
router
.route("/:id")
.get(userController.getById)
.put(userController.update)
.delete(userController.delete);
module.exports = router;
В приведенном выше фрагменте кода мы импортировали userController
и создали экспресс-маршрутизатор. С помощью экспресс-маршрутизатора мы определяем обработчики маршрутов для контроллеров.
Теперь создайте еще один файл под названием blog.js
в папке routes, чтобы определить обработчики маршрутов для контроллера blog с помощью приведенного ниже фрагмента кода.
const express = require("express");
const router = express.Router();
const blogController = require("../controller/blogController");
/* GET home page. */
router.route("/").get(blogController.getAll).post(blogController.create);
router
.route("/:id")
.get(blogController.getById)
.put(blogController.update)
.delete(blogController.delete);
module.exports = router;
Наконец, импортируйте маршруты в файл app.js и создайте промежуточное ПО для обоих маршрутов с помощью приведенного ниже фрагмента кода.
...
const userRouter = require("./routes/users");
const blogRouter = require("./routes/blog");
...
app.use('/users', userRouter);
app.use('/blog', blogRouter);
...
Тестирование API
Теперь давайте протестируем проект, чтобы убедиться, что все работает так, как ожидалось. Сначала запустите свой сервер командой, приведенной ниже.
npm start
Затем запустите Postman или любой другой инструмент тестирования API по вашему выбору.
Конечная точка пользователя
Отправьте POST-запрос на конечную точку localhost:3000/users
с приведенной ниже полезной нагрузкой, чтобы создать пользователя.
{
"name":"name",
"email":"name@gmail.com",
"password":"1234",
"bio":"I am a software dev."
}
Затем отправьте GET-запрос на ту же конечную точку, чтобы получить всех зарегистрированных пользователей. Продолжайте тестировать конечные точки других пользователей.
Конечная точка блога
Теперь отправьте POST-запрос на конечную точку localhost:3000/blog
с приведенной ниже полезной нагрузкой, чтобы создать блог для пользователя с id 1, обозначенным полем author.
{
"title":"My First Blog",
"content":"Blah Blah Blah",
"image":"Image URL",
"author":"1"
}
Затем отправьте GET-запрос на ту же конечную точку, чтобы получить все блоги.
Просмотр данных пользователя с помощью Arctype
Мы успешно создали наше приложение «Блог». Теперь давайте посмотрим на данные пользователей с помощью Arctype. Для начала запустите Arctype, перейдите на вкладку Postgres
и введите следующие учетные данные Postgres
, как показано на скриншоте ниже (это все то же самое, что мы делали с MySQL в начале):
Вы должны увидеть таблицу пользователей, таблицу блогов и таблицу knex migrations, которая ведет учет миграций, сделанных в приложении. Теперь щелкните на таблице blogs, чтобы показать блоги пользователя, как показано на скриншоте ниже:
Заключение
Создав демонстрационный проект, мы научились создавать конечные точки REST с помощью Knex и PostgreSQL. Мы начали с представления PostgreSQL и Knex и того, почему вы должны их использовать, а затем создали проект блога для демонстрации. Теперь, когда вы получили необходимые знания, как бы вы использовали конструктор запросов в своем следующем проекте? Узнайте больше о Knex на их официальном сайте и продвигайтесь еще дальше!