Връзка с базата от код
Накратко: Програмата говори с базата през драйвер: отваря връзка по адреса на базата, подава заявката, чете редовете един по един и затваря връзката.
Проблем
Страницата „Под 5 €“
Сайтът на магазина има страница с евтините продукти. Заявката за нея вече я можеш:
Само че на сайта никой не пише заявки в поле. Когато някой отвори страницата, сървърът на магазина пуска програма. Тя праща тази заявка на базата, получава 17-те реда като стойности в своя език (низ, число) и ги слага в HTML. Как точно говори програма с база?
Идеята
Петте стъпки
На всеки език е едно и също:
- Драйвер. Библиотека, която знае езика на конкретната база (SQLite, PostgreSQL, MySQL).
- Връзка. Програмата казва къде е базата (адрес) и се свързва.
- Заявка. Текстът на SQL отива към базата.
- Редове. Резултатът идва ред по ред, а програмата чете колоните му като стойности.
- Затваряне. Връзката се освобождава, дори ако по средата е станала грешка.
Ето същата заявка, с малко копие на магазина в паметта (:memory:, шест продукта, които setup пуска при старта):
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); String sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { while (rs.next()) { System.out.printf("%s: %.2f €%n", rs.getString("name"), rs.getDouble("price")); } }}Кифла с масло: 0.80 €Бял хляб: 1.20 €Прясно мляко: 1.60 €Масло: 3.20 €using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);var cmd = conn.CreateCommand();cmd.CommandText = "SELECT name, price FROM products WHERE price < 5 ORDER BY price";using var reader = cmd.ExecuteReader();while (reader.Read()){ Console.WriteLine($"{reader.GetString(0)}: {reader.GetDouble(1):F2} €");}Кифла с масло: 0.80 €Бял хляб: 1.20 €Прясно мляко: 1.60 €Масло: 3.20 €conn = sqlite3.connect(":memory:")setup(conn)sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price"for name, price in conn.execute(sql): print(f"{name}: {price:.2f} €")conn.close()Кифла с масло: 0.80 €Бял хляб: 1.20 €Прясно мляко: 1.60 €Масло: 3.20 €const db = new DatabaseSync(":memory:");setup(db);const sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price";for (const row of db.prepare(sql).all()) { console.log(`${row.name}: ${row.price.toFixed(2)} €`);}db.close();Кифла с масло: 0.80 €Бял хляб: 1.20 €Прясно мляко: 1.60 €Масло: 3.20 €Петата стъпка я прави езикът: try (…) в Java, using в C#, close() в Python и JavaScript. В Python with затваря транзакцията, а не връзката, затова тук close() е изрично.
По-точно, за любопитните
setup пуска SQL текста, който прави таблицата с шестте продукта:
static void setup(Connection conn) throws SQLException { try (Statement st = conn.createStatement()) { st.executeUpdate(""" CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Бял хляб', 1.20, NULL), (3, 'Кифла с масло', 0.80, 10), (5, 'Прясно мляко', 1.60, NULL), (7, 'Сирене', 7.90, 15), (9, 'Масло', 3.20, NULL), (17, 'Кафе на зърна', 14.90, 0); """); }}static void Setup(SqliteConnection conn){ var cmd = conn.CreateCommand(); cmd.CommandText = """ CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Бял хляб', 1.20, NULL), (3, 'Кифла с масло', 0.80, 10), (5, 'Прясно мляко', 1.60, NULL), (7, 'Сирене', 7.90, 15), (9, 'Масло', 3.20, NULL), (17, 'Кафе на зърна', 14.90, 0); """; cmd.ExecuteNonQuery();}def setup(conn): conn.executescript(""" CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Бял хляб', 1.20, NULL), (3, 'Кифла с масло', 0.80, 10), (5, 'Прясно мляко', 1.60, NULL), (7, 'Сирене', 7.90, 15), (9, 'Масло', 3.20, NULL), (17, 'Кафе на зърна', 14.90, 0); """)function setup(db) { db.exec(` CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Бял хляб', 1.20, NULL), (3, 'Кифла с масло', 0.80, 10), (5, 'Прясно мляко', 1.60, NULL), (7, 'Сирене', 7.90, 15), (9, 'Масло', 3.20, NULL), (17, 'Кафе на зърна', 14.90, 0); `);}Драйверите в примерите: sqlite-jdbc в Java (пакетът org.xerial:sqlite-jdbc), Microsoft.Data.Sqlite в C# (NuGet), а в Python и JavaScript SQLite е вграден (sqlite3, node:sqlite от Node 22). Всеки от примерите е пуснат и изходът му е точно този.
Виж го
Адресът на базата и паролата
SQLite е файл и адресът е пътят до него. Ако си направил shop.db в урока „SQL и истинските бази“ (sqlite3 shop.db < shop-bg.sql), смени :memory: с пътя и махни setup: на Java е jdbc:sqlite:shop.db, на C# Data Source=shop.db, а на Python и JavaScript просто "shop.db". Същата програма ще покаже всичките 17 продукта.
Сървърна база (PostgreSQL, MySQL, SQL Server) живее в друга програма, често на друг компютър. Адресът има хост, порт, име на базата, потребител и парола:
String url = "jdbc:postgresql://localhost:5432/shop";String password = System.getenv("SHOP_DB_PASSWORD");try (Connection conn = DriverManager.getConnection(url, "shop_app", password)) { // същите заявки}var password = Environment.GetEnvironmentVariable("SHOP_DB_PASSWORD");var url = $"Host=localhost;Port=5432;Database=shop;Username=shop_app;Password={password}";using var conn = new NpgsqlConnection(url);conn.Open();import osimport psycopg
conn = psycopg.connect(host="localhost", port=5432, dbname="shop", user="shop_app", password=os.environ["SHOP_DB_PASSWORD"])import pg from "pg";
const db = new pg.Client({ host: "localhost", port: 5432, database: "shop", user: "shop_app", password: process.env.SHOP_DB_PASSWORD });await db.connect();Паролата не е в кода. Кодът отива в git и го виждат всички с достъп, а паролата за истинския сайт е различна от тази на твоя компютър. Затова тя идва отвън: от променлива на средата, както тук, или от хранилище за тайни на хостинга.
Тези четири примера не се пускат в проверката на сайта, защото им трябва сървър с PostgreSQL. Драйверите са org.postgresql, Npgsql, psycopg и pg.
Пробвай
Заявката за друга страница
Страницата „Свършили“ в админ панела показва имената на продуктите с наличност 0, по азбучен ред. Напиши заявката, която програмата ще прати.
Решение
SELECT name FROM products WHERE stock = 0 ORDER BY name;Програмата не се променя: сменя се само текстът в sql. Затова е удобно заявката първо да се изпробва в поле като това и чак после да отиде в кода.
Идеята
От колона до стойност
Всяка колона идва като стойност от типа на езика. INTEGER става int (или long), REAL става double, TEXT става String / string / str. Интересното е NULL. В Python той е None, в JavaScript null, а в Java и C# зависи как четеш:
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT name, discount FROM products WHERE id IN (1, 3, 17)")) { while (rs.next()) { int discount = rs.getInt("discount"); String text = rs.wasNull() ? "без отстъпка" : discount + " %"; System.out.println(rs.getString("name") + ": getInt дава " + discount + ", значи " + text); } }}Бял хляб: getInt дава 0, значи без отстъпкаКифла с масло: getInt дава 10, значи 10 %Кафе на зърна: getInt дава 0, значи 0 %using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);var cmd = conn.CreateCommand();cmd.CommandText = "SELECT name, discount FROM products WHERE id IN (1, 3, 17)";using var reader = cmd.ExecuteReader();while (reader.Read()){ var text = reader.IsDBNull(1) ? "без отстъпка" : $"{reader.GetInt32(1)} %"; Console.WriteLine($"{reader.GetString(0)}: {text}");}Бял хляб: без отстъпкаКифла с масло: 10 %Кафе на зърна: 0 %conn = sqlite3.connect(":memory:")setup(conn)for name, discount in conn.execute("SELECT name, discount FROM products WHERE id IN (1, 3, 17)"): text = "без отстъпка" if discount is None else f"{discount} %" print(f"{name}: {discount!r}, значи {text}")conn.close()Бял хляб: None, значи без отстъпкаКифла с масло: 10, значи 10 %Кафе на зърна: 0, значи 0 %const db = new DatabaseSync(":memory:");setup(db);for (const row of db.prepare("SELECT name, discount FROM products WHERE id IN (1, 3, 17)").all()) { const text = row.discount === null ? "без отстъпка" : `${row.discount} %`; console.log(`${row.name}: ${row.discount}, значи ${text}`);}db.close();Бял хляб: null, значи без отстъпкаКифла с масло: 10, значи 10 %Кафе на зърна: 0, значи 0 %Бял хляб е без отстъпка (NULL), а кафето е с отстъпка 0 %. Това са различни неща. В Java getInt дава 0 и за двете и само wasNull() ги различава. В C# GetInt32 върху NULL спира с грешка, затова първо се пита IsDBNull.
Упражнение
Новите продукти
Началната страница показва новите продукти: името и цената на продуктите, добавени (added_at) от 1 юни 2025 нататък, най-новите първи, а при еднаква дата по име.
Решение
SELECT name, price FROM products WHERE added_at >= '2025-06-01' ORDER BY added_at DESC, name;С два ключа в ORDER BY: added_at DESC, name. Без втория ключ двата продукта от 1 август могат да излязат в различен ред при всяко пускане, а на сайта това личи.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: SELECT: избор на колони · WHERE: филтриране на редове · NULL: няма стойност · ORDER BY, LIMIT и DISTINCT
- Продължава в: SQL injection и параметрите · ORM и проблемът N+1 · Миграции на схемата · Пул от връзки, резервни копия и права
- Задачи по темата: Базата в програмата