Menu

Подключение к SQLite из Python, Node.js, Go и Java

Как приложение открывает базу SQLite: путь к файлу, строки подключения, драйверы для разных языков и настройки, которые лучше выставить сразу.

Подключение к SQLite — это просто открытый файл

У SQLite нет сервера. Нет демона, слушающего порт, нет хоста, к которому надо коннектиться, нет логина и пароля. «Подключение к SQLite» — это когда драйвер открывает файл на диске и начинает читать и писать в него страницы. Вот и вся модель в голове.

В каждом языке есть свой драйвер, который оборачивает сишную библиотеку SQLite. Обёртки выглядят по-разному, но детали везде одни и те же: путь к файлу базы, вызов открытия, дескриптор, на котором выполняются запросы, и закрытие, когда всё сделано.

-- Концептуально, каждый драйвер делает следующее:
-- 1. Открыть или создать файл по заданному пути.
-- 2. Получить дескриптор (handle).
-- 3. Выполнить SQL через подготовленные выражения (prepared statements).
-- 4. Закрыть дескриптор.

Дальше на странице — как это выглядит в реальном коде и какие пара настроек стоит выставить ещё до первого запроса.

Python: модуль sqlite3 из стандартной библиотеки

В Python модуль sqlite3 идёт «из коробки» — ставить ничего не нужно. Базовый каркас подключения к SQLite выглядит так:

-- Python
import sqlite3

conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
conn.execute("INSERT INTO notes (body) VALUES (?)", ("первая заметка",))
conn.commit()

for row in conn.execute("SELECT id, body FROM notes"):
    print(row)

conn.close()

Несколько важных моментов:

  • sqlite3.connect("app.db") создаёт файл, если его ещё нет. Передайте ":memory:", чтобы база жила только в оперативке.
  • sqlite3.connect("file:app.db?mode=ro", uri=True) открывает базу только на чтение через URI-форму.
  • ? в SQL — это плейсхолдер. Всегда используйте привязку параметров, никогда не склеивайте строки. Подробнее об этом — в следующей главе.
  • conn.commit() обязателен, если только вы не используете контекстный менеджер (with conn:) — он коммитит сам.

В долгоживущем приложении задайте busy timeout, чтобы конкурентные пишущие соединения ждали, а не падали с ошибкой:

-- Python
conn.execute("PRAGMA busy_timeout = 5000")   -- ждать до 5 секунд
conn.execute("PRAGMA journal_mode = WAL")    -- лучшая параллельность

Node.js: better-sqlite3

В экосистеме Node вариантов несколько, но большинство команд выбирают именно better-sqlite3. Он синхронный — и хотя для Node это звучит как ересь, на практике для SQLite так получается быстрее: запросы отрабатывают за микросекунды.

-- Node.js
const Database = require("better-sqlite3");
const db = new Database("app.db");

db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");

const insert = db.prepare("INSERT INTO notes (body) VALUES (?)");
insert.run("первая заметка");

const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);

db.close();

db.prepare(...) возвращает объект подготовленного запроса, который можно переиспользовать. .run() — для записи, .all() отдаёт все строки, .get() — одну. Шаблон знакомый по большинству SQL-драйверов.

Прагмы выставляем при старте:

-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON");   -- по умолчанию выключено, почти всегда нужно включать

foreign_keys = ON стоит упомянуть отдельно: SQLite не проверяет внешние ключи, пока вы явно не попросите — причём для каждого подключения отдельно. Забудете включить — и ваши REFERENCES превращаются в чистую декорацию.

Go: database/sql и драйвер для SQLite

Стандартный пакет database/sql в Go не привязан к конкретному драйверу. Для SQLite обычно берут один из двух: modernc.org/sqlite (чистый Go, без CGO) либо github.com/mattn/go-sqlite3 (с CGO).

-- Go
import (
    "database/sql"
    _ "modernc.org/sqlite"
)

db, err := sql.Open("sqlite", "app.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
if err != nil { panic(err) }
defer db.Close()

_, err = db.Exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
_, err = db.Exec("INSERT INTO notes (body) VALUES (?)", "первая заметка")

rows, _ := db.Query("SELECT id, body FROM notes")
defer rows.Close()
for rows.Next() {
    var id int; var body string
    rows.Scan(&id, &body)
    fmt.Println(id, body)
}

Строка запроса после имени файла — это способ, которым драйвер передаёт прагмы при подключении. Формат зависит от конкретного драйвера, так что загляните в документацию того, что выберете.

sql.Open на самом деле не открывает соединение — оно устанавливается при первом запросе. db здесь — это пул соединений. Для SQLite обычно достаточно небольшого пула, а под нагрузкой на запись часто разумно вообще выставить db.SetMaxOpenConns(1).

Java: подключение SQLite через JDBC

Стандартом де-факто считается драйвер org.xerial:sqlite-jdbc. JDBC-строка подключения выглядит как jdbc:sqlite:<path>:

-- Java
import java.sql.*;

try (Connection conn = DriverManager.getConnection("jdbc:sqlite:app.db")) {
    try (Statement st = conn.createStatement()) {
        st.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
        st.execute("PRAGMA journal_mode = WAL");
        st.execute("PRAGMA busy_timeout = 5000");
    }

    try (PreparedStatement ps = conn.prepareStatement("INSERT INTO notes (body) VALUES (?)")) {
        ps.setString(1, "первая заметка");
        ps.executeUpdate();
    }

    try (PreparedStatement ps = conn.prepareStatement("SELECT id, body FROM notes");
         ResultSet rs = ps.executeQuery()) {
        while (rs.next()) System.out.println(rs.getInt(1) + " " + rs.getString(2));
    }
}

In-memory: jdbc:sqlite::memory:. Только для чтения: добавьте ?open_mode=1 либо воспользуйтесь объектом SQLiteConfig.

PHP: PDO

В PDO строка подключения к SQLite (DSN) выглядит как sqlite:<path>:

-- PHP
$db = new PDO("sqlite:app.db");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$db->exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
$db->exec("PRAGMA journal_mode = WAL");
$db->exec("PRAGMA busy_timeout = 5000");

$stmt = $db->prepare("INSERT INTO notes (body) VALUES (?)");
$stmt->execute(["первая заметка"]);

foreach ($db->query("SELECT id, body FROM notes") as $row) {
    echo $row["id"] . " " . $row["body"] . "\n";
}

sqlite::memory: — это база данных в памяти. Обязательно выставляйте ATTR_ERRMODE в режим исключений: молчаливые ошибки потом крайне неприятно отлаживать.

Строка подключения и пути к файлам

В разных драйверах строка подключения к SQLite встречается в двух вариантах:

  • Обычный путь: app.db, ./data/app.db, /var/lib/myapp/app.db. Относительные пути считаются от текущей рабочей директории процесса — а это редко то, что нужно на проде. Лучше используйте абсолютные пути.
  • URI-формат: file:app.db?mode=rwc&cache=shared. Здесь можно задать флаги: mode=ro (только чтение), mode=rwc (чтение-запись-создание, по умолчанию), cache=shared, nolock=1.

Особые значения, которые точно встретятся:

  • :memory: — приватная база в оперативной памяти. У каждого подключения своя.
  • file::memory:?cache=shared — база в памяти, которую могут совместно использовать несколько подключений в рамках одного процесса.
  • "" (пустая строка) — приватная временная база на диске, удаляется при закрытии.

В JDBC к URI добавляется префикс jdbc:sqlite:. PDO использует sqlite:. Драйверы для Go и модуль sqlite3 в Python принимают путь или URI напрямую.

А как же пулы соединений?

SQLite — однопользовательская на запись база. В любой момент времени блокировкой на запись владеет ровно одно подключение, остальные ждут своей очереди. Пул из кучи писателей не ускорит запись — вы просто получите больше претендентов на один и тот же лок.

И всё же небольшой пул бывает полезен:

  • Для параллельных чтений в режиме WAL, где читатели не блокируют ни друг друга, ни писателя.
  • Чтобы избежать head-of-line blocking — ситуации, когда один медленный запрос тормозит всё приложение.

Разумные настройки по умолчанию для веб-приложения:

  • Включённый WAL-режим.
  • busy_timeout в несколько секунд, чтобы при конкуренции запросы вежливо ждали, а не падали с ошибкой.
  • Размер пула: 1 писатель + N читателей, либо вообще одно общее подключение, если трафик невелик.
  • Внешние ключи включены на каждом подключении.
-- Применяйте это для каждого нового соединения:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL;   -- безопасно с WAL; быстрее, чем FULL

synchronous = NORMAL — это типичный спутник WAL: переживает падения приложения, чуть менее строг при падении ОС и заметно быстрее, чем дефолтный FULL.

Закрытие подключений (и почему это важно)

В каждом драйвере есть свой метод закрытия: conn.close(), db.Close(), db.close(). Если его не вызывать, утекают файловые дескрипторы, а WAL-файл может бесконтрольно разрастаться.

В долгоживущих сервисах гораздо разумнее держать одно подключение (или пул) на всё время жизни процесса, а не открывать и закрывать его вокруг каждого запроса. Само подключение к SQLite стоит дёшево, но вот заново прогонять PRAGMA при каждом открытии — это лишняя работа, про которую к тому же легко забыть.

-- Python — соединение на процесс, переиспользуется между запросами
DB = sqlite3.connect("app.db", check_same_thread=False)
DB.execute("PRAGMA journal_mode = WAL")
DB.execute("PRAGMA busy_timeout = 5000")
DB.execute("PRAGMA foreign_keys = ON")

Если говорить о Python, то параметр check_same_thread=False понадобится, когда соединение используется из нескольких потоков, — и тогда же стоит задуматься о блокировке или пуле, чтобы вызовы шли по очереди.

Чеклист перед выкаткой в прод

Прежде чем направлять реальный трафик в базу SQLite, пройдитесь по списку:

  • Указывайте абсолютный путь к файлу базы.
  • Включите режим WAL (PRAGMA journal_mode = WAL).
  • Выставьте busy_timeout в диапазоне 2–10 секунд.
  • Включайте внешние ключи на каждом соединении.
  • Используйте подготовленные запросы с биндингом параметров — никакой склейки строк.
  • Убедитесь, что каталог с базой доступен процессу на запись (в режиме WAL рядом с основным файлом SQLite создаёт файлы -wal и -shm).
  • Подумайте о бэкапах заранее, а не когда уже припекло, — про VACUUM INTO и команду .backup поговорим дальше.

Дальше: миграции

Подключиться — это самое простое. Сложнее эволюционировать схему со временем, не правя продовую базу руками. Миграции — это как раз способ превратить ALTER TABLE в повторяемый процесс под контролем версий. Об этом — на следующей странице.

Часто задаваемые вопросы

Как подключиться к базе SQLite из кода?

Достаточно указать драйверу путь к файлу. В Python это sqlite3.connect('app.db'), в Node — new Database('app.db') через better-sqlite3, в Go — sql.Open("sqlite", "app.db"). Сервера у SQLite нет, поэтому «подключение» по сути сводится к открытию файла — если файла нет, SQLite создаст его сам.

Как выглядит строка подключения к SQLite?

Большинство драйверов принимают либо обычный путь к файлу (./data/app.db), либо URI-форму (file:app.db?mode=rwc&cache=shared). URI удобен тем, что через него можно задать флаги: режим только для чтения, общий кэш или базу :memory: в оперативке. JDBC использует формат jdbc:sqlite:app.db, PDO — sqlite:app.db.

Нужен ли SQLite пул соединений?

Обычно не в том смысле, как для Postgres или MySQL. SQLite сериализует запись на уровне всей базы, так что несколько писателей в пуле всё равно не дадут прироста. Небольшой пул имеет смысл для параллельных чтений, особенно в режиме WAL. Многим приложениям хватает одного общего соединения с PRAGMA journal_mode=WAL и адекватным busy_timeout.

Как избавиться от ошибки 'database is locked'?

Поставьте busy timeout, чтобы драйвер ждал, а не падал сразу: PRAGMA busy_timeout = 5000 (миллисекунды). Включите режим WAL через PRAGMA journal_mode=WAL — тогда читатели не блокируют писателей. И главное: держите транзакции короткими и не оставляйте открытую транзакцию записи, пока выполняете долгие операции вне базы.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ