Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

База данных SQLite

Пока статьи хранятся в массиве, они пропадают при каждом перезапуске сервера. Нужна база данных. Для учебного блога удобна SQLite: вся база — один файл в папке проекта, отдельный сервер не нужен. На Node.js с ней работает пакет better-sqlite3.

Установка

npm install better-sqlite3
npm install -D @types/better-sqlite3

Второй пакет содержит типы для TypeScript и нужен только при разработке.

Подключение и таблицы

Создайте src/db.ts:

// src/db.ts
import Database from 'better-sqlite3'
 
export const db = new Database(process.env.DB_FILE ?? '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 type Post = {
  id: number
  title: string
  body: string
  created_at: string
}

При первом импорте этого модуля в корне проекта появится файл blog.db с двумя таблицами. IF NOT EXISTS не даёт создать таблицы повторно при следующих запусках. Строка с pragma включает проверку связей: без неё SQLite не удалит комментарии вместе со статьёй. Добавьте blog.db в .gitignore.

Запросы

Запрос сначала готовят методом prepare(), а затем выполняют одним из трёх методов:

МетодДля чегоРезультат
.all(...)SELECT нескольких строкмассив объектов
.get(...)SELECT одной строкиобъект или undefined
.run(...)INSERT, UPDATE, DELETEchanges и lastInsertRowid

Знаки ? в запросе заменяются аргументами по порядку.

Чтение статей

Используйте базу в src/routes/posts.ts:

// src/routes/posts.ts
import { Hono } from 'hono'
import { db, type Post } from '../db.js'
 
const posts = new Hono()
 
posts.get('/', (c) => {
  const rows = db.prepare('SELECT * FROM posts ORDER BY id DESC').all() as Post[]
  return c.json(rows)
})
 
export default posts

http://localhost:3000/posts пока вернёт пустой массив [] — таблица пуста. Приведение as Post[] нужно, потому что библиотека не знает, какие столбцы вернёт запрос.

Добавление статьи

// src/routes/posts.ts
const result = db
  .prepare('INSERT INTO posts (title, body) VALUES (?, ?)')
  .run('Первая статья', 'Привет из SQLite')
 
console.log(result.lastInsertRowid) // номер новой строки

После такого вызова список статей вернёт одну запись. Полный набор маршрутов для создания, изменения и удаления — в следующем уроке.

Синхронная работа

better-sqlite3 работает синхронно: методы сразу возвращают результат, без await. Для SQLite это нормально и быстро — файл лежит рядом с программой.

Другие платформы

Пакет better-sqlite3 работает только на Node.js. На Cloudflare Workers вместо него используют D1 — базу SQLite от Cloudflare, она подключается как binding и доступна через c.env. SQL-запросы при этом остаются почти такими же, но методы вызова другие и асинхронные.

Частая ошибка

Данные подставляют в SQL через шаблон строки:

// src/routes/posts.ts
db.prepare(`SELECT * FROM posts WHERE id = ${c.req.param('id')}`).get()

Если в адресе окажется кусок SQL, он выполнится. Это называется SQL-инъекцией. Всегда передавайте значения через ? и аргументы метода: db.prepare('SELECT * FROM posts WHERE id = ?').get(id).

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.