PHP / PDO ба өгөгдлийн сан

PDO ба өгөгдлийн сан

Вэб програм бараг бүгдэд өгөгдлийг хадгалж, уншдаг. Файлд хадгалах нь жижиг тохиолдолд хангалттай боловч жинхэнэ апп нь өгөгдлийн сан (database) ашигладаг. PHP-д өгөгдлийн сантай ажиллах хамгийн зөв арга бол PDO (PHP Data Objects) юм. PDO нь нэг интерфейсээр MySQL, PostgreSQL, SQLite болон бусад олон өгөгдлийн сантай ажилладаг — нэг аргаар суралцаж, хаана ч хэрэглэж болно.

PDO холболт нээх

PDO объект үүсгэхдээ DSN (Data Source Name) дамжуулна. DSN нь өгөгдлийн сангийн төрөл, хост, нэр зэргийг агуулсан холбооны мэдээлэл:

php
<?php
// SQLite — файл дээр суурилсан, суулгах шаардлагагүй
try {
    $pdo = new PDO('sqlite:myapp.db');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    echo "SQLite холбогдлоо.\n";
} catch (PDOException $e) {
    echo "Холболтын алдаа: " . $e->getMessage();
}

// MySQL — жинхэнэ серверт холбогдох
try {
    $dsn = "mysql:host=localhost;dbname=myapp;charset=utf8mb4";
    $pdo = new PDO($dsn, "root", "нууц_үг");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    echo "MySQL холбогдлоо.\n";
} catch (PDOException $e) {
    echo "Холболтын алдаа: " . $e->getMessage();
}
?>

PDO::ERRMODE_EXCEPTION нь алдаа гарахад PDOException шидэхийг тохируулна — try/catch-аар зохицуулж болдог болгоно. PDO::FETCH_ASSOC нь мөр бүрийг нэрлэгдсэн массив болгон буцаана.

Холболтыг тусдаа файлд хадгалах

Холболтын кодыг нэг дор байлгах нь шилдэг дадал. Ашиглах газар бүрт нэг дуудна:

php
<?php
// файл: lib/database.php
function өгөгдлийн_сан_авах(): PDO {
    static $pdo = null;

    if ($pdo !== null) {
        return $pdo; // Байгаа холболтыг буцаана — шинэ нээхгүй
    }

    $хост   = $_ENV['DB_HOST']   ?? 'localhost';
    $нэр    = $_ENV['DB_NAME']   ?? 'myapp';
    $хэрэглэгч = $_ENV['DB_USER'] ?? 'root';
    $нууц   = $_ENV['DB_PASS']   ?? '';

    $dsn = "mysql:host={$хост};dbname={$нэр};charset=utf8mb4";

    $pdo = new PDO($dsn, $хэрэглэгч, $нууц, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ]);

    return $pdo;
}
?>

static $pdo = null нь функцийг хэдэн удаа дуудсан ч зөвхөн нэг холболт нээгддэг болгоно — энэ нь Singleton загварын хялбар хэлбэр юм.

Prepared statement — аюулгүй асуулга

PDO-ийн хамгийн чухал хэрэгсэл бол prepared statement юм. Хэрэглэгчийн оруулсан утгыг SQL-д шууд оруулахгүй, тусдаа дамжуулах замаар SQL injection халдлагаас бүрэн хамгаалдаг:

php
<?php
$pdo = өгөгдлийн_сан_авах();

// ❌ БУРУУ — SQL injection-д эмзэг
$нэр = $_POST['нэр']; // хэрэглэгч "'OR'1'='1" оруулбал бүх мөр задарна
$sql = "SELECT * FROM users WHERE нэр = '{$нэр}'"; // ХЭЗЭЭ ЧИ БҮҮЛ

// ✅ ЗӨВ — Prepared statement
$sql  = "SELECT * FROM users WHERE нэр = :нэр";
$stmt = $pdo->prepare($sql);
$stmt->execute([':нэр' => $нэр]); // Утга тусдаа дамжина
$хэрэглэгч = $stmt->fetch();

// Асуулта тэмдэглэгч (?) ашиглан
$stmt = $pdo->prepare("SELECT * FROM users WHERE нэр = ? AND нас > ?");
$stmt->execute([$нэр, 18]);
$үр_дүн = $stmt->fetchAll();
?>

Prepared statement-т хэрэглэгчийн утга ', ;, -- зэрэг аюултай тэмдэгтүүд хамаагүй — PHP нь тэдгээрийг автоматаар аюулгүй болгоно.

Хүснэгт үүсгэх ба анхны өгөгдөл

SQLite ашиглан туршилтын өгөгдлийн сан бүтээх жишээ — MySQL дээр яг адилхан ажилладаг:

php
<?php
$pdo = new PDO('sqlite:сургалт.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Хүснэгт үүсгэх
$pdo->exec("
    CREATE TABLE IF NOT EXISTS хэрэглэгчид (
        id      INTEGER PRIMARY KEY AUTOINCREMENT,
        нэр     TEXT    NOT NULL,
        имэйл   TEXT    NOT NULL UNIQUE,
        нас     INTEGER,
        огноо   TEXT    DEFAULT CURRENT_TIMESTAMP
    )
");

echo "Хүснэгт үүслээ.\n";

// Туршилтын өгөгдөл оруулах
$хэрэглэгчид = [
    ['Болд',   'bold@example.mn',   25],
    ['Сарнай', 'sarnai@example.mn', 22],
    ['Тамир',  'tamir@example.mn',  30],
];

$stmt = $pdo->prepare("INSERT INTO хэрэглэгчид (нэр, имэйл, нас) VALUES (?, ?, ?)");

foreach ($хэрэглэгчид as [$нэр, $имэйл, $нас]) {
    $stmt->execute([$нэр, $имэйл, $нас]);
}

echo "Өгөгдөл оруулав.\n";

// Шалгах
$stmt = $pdo->query("SELECT * FROM хэрэглэгчид");
$бүгд = $stmt->fetchAll();

foreach ($бүгд as $хэрэглэгч) {
    echo "{$хэрэглэгч['id']}. {$хэрэглэгч['нэр']}{$хэрэглэгч['имэйл']}\n";
}
?>

exec() нь үр дүн буцаадаггүй SQL (CREATE, DROP) дуудахад, query() нь параметргүй SELECT-д, prepare() нь параметртэй SQL-д ашиглагддаг.

PDO-ийн fetch горимууд

php
<?php
$stmt = $pdo->query("SELECT * FROM хэрэглэгчид");

// FETCH_ASSOC — нэрлэгдсэн массив (хамгийн түгээмэл)
$мөр = $stmt->fetch(PDO::FETCH_ASSOC);
// ['id' => 1, 'нэр' => 'Болд', 'имэйл' => '...']

// FETCH_OBJ — объект болгон авна
$stmt = $pdo->query("SELECT * FROM хэрэглэгчид");
$объект = $stmt->fetch(PDO::FETCH_OBJ);
echo $объект->нэр; // Болд

// fetchAll() — бүх мөрийг нэг дор авна
$stmt = $pdo->query("SELECT * FROM хэрэглэгчид");
$бүгд = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo count($бүгд) . " хэрэглэгч байна.\n";

// fetchColumn() — нэг баганын утга
$тоо = $pdo->query("SELECT COUNT(*) FROM хэрэглэгчид")->fetchColumn();
echo "Нийт: {$тоо}\n";
?>

Дараагийн хичээлд:

PDO ашиглан бүрэн CRUD (Create, Read, Update, Delete) үйлдлүүдийг хэрэгжүүлнэ. Бодит жишээн дээр хэрэглэгчийн систем бүтээж, transaction хэрхэн ашиглахыг судална.