عرض عملي: إنشاء قاعدة بيانات Library مع القيود
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
This lesson is a guided build. You will combine everything from Lessons 1 to 5 into one result: the Library database, created by a script that can be run again from scratch, with constraints, a few seed rows, deliberately rejected bad rows, and a rollback. It is also the preparation for the module's lab, BEG-051-LAB1, where you do the same on your own.
In the library picture: the building is already open (Lesson 1), you know the corridors (Lesson 2), you have a room and a key (Lesson 3), and now you fit the shelves and put rules on the doors (Lessons 4 and 5), writing it all in a logbook so that anyone can rebuild the room.
Why this matters: a re-runnable script is how real teams create and refresh databases. If your database exists only because of clicks you made once, it is a pet; if it comes from a script, it is reproducible.
This lesson builds on all of BEG-051 and on the Library tables from BEG-049 and BEG-050 (authors, books, members, loans).
Learning Objectives
By the end of this lesson you will be able to:
- Create the
library_appuser andlibrarydatabase and connect as that user. - Write a
schema.sqlthat drops and recreates the four Library tables, withNOT NULL,UNIQUE,CHECK, and foreign keys. - Order
DROPandCREATEstatements correctly because of foreign keys. - Run the script twice and explain why the second run works.
- Provoke each constraint on purpose and record the error messages.
- Demonstrate a rollback and a commit with the Library data.
Written Explanation
Step 1: user and database (once, as postgres)
This is the only step that needs the superuser. Run it once:
CREATE USER library_app WITH PASSWORD 'change-me';
CREATE DATABASE library OWNER library_app;Use a throwaway password for a local lab, or set it with \password library_app so it does not appear in your history. If the user or database already exists, you will get an "already exists" error; the statements are not re-runnable by design, and that is fine because they are setup, not part of the schema. Do not put real passwords in files you commit.
From now on, connect as the application user:
psql -U library_app -d libraryStep 2: the schema script
Save this file as schema.sql. It was run against PostgreSQL 18.3 for this lesson.
-- schema.sql: rebuilds the Library tables from scratch.
-- Drop in reverse dependency order (children before parents).
DROP TABLE IF EXISTS loans;
DROP TABLE IF EXISTS books;
DROP TABLE IF EXISTS members;
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
country TEXT
);
CREATE TABLE books (
id INTEGER PRIMARY KEY,
isbn TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
author_id INTEGER NOT NULL REFERENCES authors (id),
copies INTEGER NOT NULL CHECK (copies >= 0)
);
CREATE TABLE members (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE loans (
id INTEGER PRIMARY KEY,
book_id INTEGER NOT NULL REFERENCES books (id),
member_id INTEGER NOT NULL REFERENCES members (id),
loan_date DATE NOT NULL DEFAULT CURRENT_DATE
);
-- Seed data: parents first, then children.
INSERT INTO authors (id, name, country) VALUES
(1, 'Ursula Le Guin', 'USA'),
(2, 'Chinua Achebe', 'Nigeria');
INSERT INTO books (id, isbn, title, author_id, copies) VALUES
(1, '978-0-00-000001-1', 'The Dispossessed', 1, 3),
(2, '978-0-00-000002-2', 'Things Fall Apart', 2, 2);
INSERT INTO members (id, name, email) VALUES
(1, 'Sara', 'sara@example.com'),
(2, 'Omar', 'omar@example.com');
INSERT INTO loans (id, book_id, member_id) VALUES (1, 1, 1);(The ISBN values are made up for the exercise.) Three design points:
- Order matters. A
DROP TABLE bookswould fail whileloansstill points at it, soloansgoes first. ForCREATEit is the reverse:authorsandmembersbeforebooksandloans, because a foreign key can only point at a table that exists. Seed rows follow the same parent-first order, since a book needs its author to exist. DROP TABLE IF EXISTSmakes the first lines harmless on a fresh database (PostgreSQL prints a notice and skips) and clean on a database that already has the tables. That is what makes the script re-runnable.- Constraints live in the table definitions, not in a separate step:
NOT NULL,UNIQUE,CHECK, andREFERENCES.
Step 3: run it, twice
psql -U library_app -d library -v ON_ERROR_STOP=1 -f schema.sqlOn a fresh database the first lines print NOTICE: table "loans" does not exist, skipping, then DROP TABLE lines, CREATE TABLE four times, and INSERT 0 2, INSERT 0 2, INSERT 0 2, INSERT 0 1. Run the same command again: the notices disappear (the tables exist), the drops succeed, and the same CREATE TABLE and INSERT lines follow. Check:
SELECT (SELECT count(*) FROM authors) a, (SELECT count(*) FROM books) b,
(SELECT count(*) FROM members) m, (SELECT count(*) FROM loans) l; a | b | m | l
---+---+---+---
2 | 2 | 2 | 1After two runs the counts are 2, 2, 2, and 1, not doubled: each run starts clean. For a stronger test, as postgres run DROP DATABASE library; and CREATE DATABASE library OWNER library_app;, then run the script again. It works from nothing, which is the acceptance rule of the lab.
The -v ON_ERROR_STOP=1 option stops the script at the first error. We confirmed that it stops on a failing statement in Lesson 2.
Step 4: the planned mistake, a loan for a member who does not exist
INSERT INTO loans (id, book_id, member_id) VALUES (2, 1, 999);ERROR: insert or update on table "loans" violates foreign key constraint "loans_member_id_fkey"
DETAIL: Key (member_id)=(999) is not present in table "members".This is the error the lab asks you to record. Read it: the table (loans), the rule (a foreign key named loans_member_id_fkey), and the value (999). The row was not inserted.
Then break the other rules in turn (use ids that are free, such as 3). The messages are in this lesson's real run:
| Bad statement (idea) | Error first line |
|---|---|
Book with a NULL title | null value in column "title" of relation "books" violates not-null constraint |
| Book with an ISBN that exists | duplicate key value violates unique constraint "books_isbn_key" |
Book with copies = -1 | new row for relation "books" violates check constraint "books_copies_check" |
| Delete member 1 who has a loan | update or delete on table "members" violates foreign key constraint "loans_member_id_fkey" on table "loans" |
Step 5: the rollback
BEGIN;
UPDATE books SET copies = copies - 1 WHERE id = 1;
SELECT id, copies FROM books WHERE id = 1; -- shows 2
ROLLBACK;
SELECT id, copies FROM books WHERE id = 1; -- shows 3 againSeed copies are 3, so the first SELECT shows 2 and the second shows 3. To show a commit, end with COMMIT; instead and check that the value stays at 2 (then re-run schema.sql to restore the starting state).
Examples
Example 1: re-run proves the script (simple)
Run schema.sql twice and then \dt. You get four tables owned by library_app, in schema public, and the same row counts. Nothing from the first run is left behind and nothing duplicates.
Example 2: constraint fails, data intact (practical)
INSERT INTO books (id, isbn, title, author_id, copies)
VALUES (3, '978-0-00-000003-3', 'Neg', 1, -1);
SELECT count(*) FROM books;The insert fails with the books_copies_check message, and the count is still 2. A failed statement never leaves a half-inserted row.
Example 3: a lending transaction that goes wrong, then right (realistic)
BEGIN;
UPDATE books SET copies = copies - 1 WHERE id = 1;
INSERT INTO loans (id, book_id, member_id) VALUES (2, 1, 999);
ROLLBACK;
SELECT copies FROM books WHERE id = 1; -- still 3
BEGIN;
UPDATE books SET copies = copies - 1 WHERE id = 1;
INSERT INTO loans (id, book_id, member_id) VALUES (2, 1, 2);
COMMIT;
SELECT copies FROM books WHERE id = 1; -- 2
SELECT count(*) FROM loans; -- 2The first attempt fails at the foreign key and everything rolls back; the second, with a real member, commits both changes together. This pair is a good demonstration for the lab.
Backend Insight
The script you just wrote is the beginning of what teams call a schema migration: a version-controlled file that changes the database. Later modules introduce tools that run numbered migration files in order, but the principle is already here: the database's structure comes from text in Git, not from memory. The "drop everything and rebuild" shape is right for a learning database and for tests, where throwing data away is the point. It is dangerous in production, where the data is the product, so real systems use scripts that alter structure without dropping tables. Know which one you are writing.
The same script gives every developer an identical database, which removes a whole class of "it works on my machine" problems.
Common Mistakes
Mistake 1: Dropping parents before children
DROP TABLE books while loans still references it fails with "cannot drop table books because other objects depend on it". Always drop in reverse order of creation.
Mistake 2: Running the script as postgres "to avoid permission trouble"
The tables would then be owned by postgres, and library_app might not be allowed to use them. Run it as the application user so the ownership and rights are right.
Mistake 3: A script that works only once
A CREATE TABLE without a matching DROP TABLE IF EXISTS fails on the second run with "already exists". Test by running twice, and, at least once, on a freshly created database.
Mistake 4: Real passwords or customer data in the committed script
Seed data should be fake. Passwords belong in a secrets store, not in schema.sql.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Why does schema.sql drop loans before books?
أظهر الحل A1
A1. loans has foreign keys pointing at books (and members), so books cannot be dropped while loans still exists. Children go first.
A2. What does DROP TABLE IF EXISTS do when the table is missing?
أظهر الحل A2
A2. It prints a notice and skips the statement, with no error, so the script continues.
A3. Which constraint rejects a loan for member 999?
أظهر الحل A3
A3. The foreign key on loans.member_id (named loans_member_id_fkey by default).
Level B: Application
B1. Add a table branches (id primary key, name required and unique) and a column branch_id to loans that refers to it as required. Say where in the script each change goes and why.
أظهر الحل B1
B1. Add DROP TABLE IF EXISTS branches; after DROP TABLE IF EXISTS loans; (children before parents, so loans first). Add CREATE TABLE branches (...) before CREATE TABLE loans (parents first). In loans, add branch_id INTEGER NOT NULL REFERENCES branches (id). The seed must include a branch row before any loan row, and the seed loan must give a branch_id.
B2. Write the three lines that prove a rollback leaves copies unchanged for book 2 (a transaction with an update, a check inside, and a check after the rollback).
أظهر الحل B2
B2.
BEGIN;
UPDATE books SET copies = copies - 1 WHERE id = 2;
SELECT copies FROM books WHERE id = 2; -- one less
ROLLBACK;
SELECT copies FROM books WHERE id = 2; -- original valueB3. After running the script a second time, SELECT count(*) FROM loans; returns 1, not 2. Explain.
أظهر الحل B3
B3. The script drops and recreates every table, so each run starts from empty tables, and the seed inserts one loan again. Counts do not accumulate.
Level C: Challenge
C1. The script runs fine on your machine but fails for a classmate with "relation authors already exists" on a line after the drops. Name two likely causes and how you would find out which.
أظهر الحل C1
C1. Possible causes: a DROP line is missing or in the wrong order, or an earlier drop failed (for instance, the table is owned by another user, such as postgres, so library_app could not drop it); or the classmate ran a different or older version of the file. Find out by reading the first error with -v ON_ERROR_STOP=1 (it shows the line number) and by checking \dt for the owner column.
C2. Design a short "acceptance run" for a reviewer to check your lab: the exact commands, in order, and what they should see at each step.
أظهر الحل C2
C2. For example: (1) as postgres, DROP DATABASE IF EXISTS library; then create the user if missing and CREATE DATABASE library OWNER library_app; (2) as library_app, psql -d library -v ON_ERROR_STOP=1 -f schema.sql: expect four CREATE TABLE and the INSERT lines, no ERROR. (3) Run it again: same result. (4) Run each planned bad statement: expect the five error messages recorded in the notes. (5) Run the rollback demo: value changes inside, returns after. The reviewer compares the recorded outputs with their own.
Mini Challenge
Build the lab deliverable on your own machine:
- A
schema.sqlthat drops and creates the four Library tables, with at least oneNOT NULL,UNIQUE,CHECK, and foreign key each, plus seed rows. - A file
notes.mdwith the rejected statements and their error messages, copied from real runs. - A rollback demonstration with the values before, inside, and after.
Check your work: you ran the script twice, and once on a database you dropped and created again; the script contains no real password; every constraint in your notes has a matching error.
Summary & Next Lesson
Key takeaways
- Create the user and database once as
postgres, then work aslibrary_app. schema.sqldrops children before parents and creates parents before children;DROP TABLE IF EXISTSmakes it re-runnable.- Constraints belong in the table definitions, and the database rejects bad rows with errors that name the rule and the value.
- A transaction lets you try a change and roll it back; failed statements leave the data as it was.
- A script in Git, not clicks, is what makes a database reproducible.
Next lesson
This is the last lesson of the module. The next step is the lab BEG-051-LAB1, "Library database live", where you build and demonstrate this on your own.
Self-check
- Why must
DROPandCREATEbe in opposite orders? - What makes the script safe to run twice?
- Which user should run
schema.sql, and why? - What three facts does a foreign key error give you?
- What does a rollback demonstration show?
أظهر الإجابات
- A table that others reference cannot be dropped first, and a foreign key can only point at an existing table, so create parents first and drop children first.
- Each table is dropped with
DROP TABLE IF EXISTSbefore it is created, so every run starts clean. library_app, so the tables are owned by the application user with the right permissions.- The table, the constraint name, and the offending value.
- That a change is visible inside the transaction and gone after
ROLLBACK.
References
- PostgreSQL documentation, Constraints. Supports: the constraint kinds used in
schema.sqland the defaultNO ACTIONbehavior for deletes referenced by a foreign key. - PostgreSQL documentation, Transactions (tutorial). Supports:
BEGIN,COMMIT,ROLLBACK, and each statement outside a block being its own transaction. - PostgreSQL documentation, psql. Supports:
-fand-coptions and their error line numbers. - The script, both runs, the counts, the drop-and-recreate test, and every error message were run on a local PostgreSQL 18.3 test server. The wording of messages can differ in other versions.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.