База данных SQLite
Массив в памяти очищается при каждом перезапуске сервера. Чтобы статьи сохранялись, нужна база данных. SQLite хранит всю базу в одном файле и не требует отдельного сервера, а пакет better-sqlite3 даёт к ней простой доступ из Node.js. Запросы пишутся на языке SQL.
Установка
npm install better-sqlite3Пакет содержит скомпилированную часть, поэтому установка может занять минуту.
Подключение и таблицы
Создайте файл src/db.js. Он открывает базу и создаёт таблицы, если их ещё нет:
// src/db.js
import Database from 'better-sqlite3';
const db = new Database('blog.db');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author TEXT NOT NULL,
text TEXT NOT NULL
);
`);
export default db;При первом запуске в корне проекта появится файл blog.db. Поле post_id связывает комментарий со статьёй, а ON DELETE CASCADE удалит комментарии вместе со статьёй.
Модель
Все запросы к таблицам собирают в модель. Создайте src/models/post.js:
// src/models/post.js
import db from '../db.js';
export function findAll() {
return db.prepare('SELECT * FROM posts ORDER BY id DESC').all();
}
export function findById(id) {
return db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
}
export function create({ title, body }) {
const result = db.prepare('INSERT INTO posts (title, body) VALUES (?, ?)').run(title, body);
return Number(result.lastInsertRowid);
}
export function findComments(postId) {
return db.prepare('SELECT * FROM comments WHERE post_id = ? ORDER BY id').all(postId);
}
export function addComment(postId, { author, text }) {
db.prepare('INSERT INTO comments (post_id, author, text) VALUES (?, ?, ?)').run(postId, author, text);
}prepare() готовит запрос, а методы выполняют его: all() возвращает массив строк, get() — одну строку или undefined, run() — сведения об изменении. Знаки ? заменяются аргументами по порядку.
Использование в контроллере
Контроллер теперь берёт данные из модели вместо массива:
// src/controllers/postsController.js
import * as Post from '../models/post.js';
export function index(req, res) {
res.render('posts/index', { title: 'Все статьи', posts: Post.findAll() });
}
export function show(req, res) {
const post = Post.findById(Number(req.params.id));
if (!post) {
return res.status(404).render('404', { title: 'Не найдено' });
}
res.render('posts/show', { title: post.title, post, comments: Post.findComments(post.id) });
}
export function storeComment(req, res) {
Post.addComment(Number(req.params.id), req.body);
res.redirect(`/posts/${req.params.id}`);
}Маршрут для комментариев: router.post('/:id/comments', postsController.storeComment), а форма на странице статьи отправляет поля author и text на /posts/<%= post.id %>/comments. Создайте пару статей и перезапустите сервер — они останутся на месте.
Частая ошибка
Никогда не вставляйте данные посетителя в SQL через шаблонную строку:
// src/models/post.js
// Неправильно: значение из адреса попадает прямо в SQL.
db.prepare(`SELECT * FROM posts WHERE id = ${id}`).get();Вместо числа в адрес можно подставить кусок SQL и прочитать или удалить чужие данные — это SQL-инъекция. Всегда используйте знаки ? и передавайте значения аргументами: better-sqlite3 подставит их безопасно.