Grafikus C# – Windows Forms tanfolyam › 3.11 lecke

Windows Forms · 3.11 lecke · záró projekt

Mini projekt: iskolai könyvtár – nyilvántartás, kölcsönzés, késések

A 3. fejezet záró projektjében mindent összeteszel, amit az adatbázisokról tanultál: három tábla és kapcsolataik, biztonságos paraméteres lekérdezések, JOIN, szűrés és keresés, rendezett szerkezet, tranzakció és hibakezelés. A téma egy iskolai könyvtár: könyvek, olvasók és kölcsönzések, késések kimutatásával. Ha inkább felhasználói nyilvántartást építenél (például eszköz- vagy tagnyilvántartást), a lépések ugyanazok: más a téma, ugyanaz a szerkezet.

  • kb. 4–6 óra (több alkalomra bontva)
  • haladó szint
  • záró projekt
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A projekt végére

  • megtervezel és létrehozol egy három táblás adatbázist kulcsokkal;
  • a könyvek kezelését (felvétel, módosítás, törlés) rétegekre bontva, párbeszédablakkal építed fel;
  • a kölcsönzést tranzakcióban, szabályok ellenőrzésével valósítod meg;
  • JOIN lekérdezésekkel listázod az aktuális kölcsönzéseket, a késéseket és a szabad példányokat;
  • kereső és statisztika készül;
  • tesztlistával és önértékeléssel ellenőrzöd a munkádat.

Mielőtt elkezded

Ehhez a projekthez a 3. fejezet összes leckéje kell, különösen a 3.5 (szűrés), 3.7 (paraméterek, dátumok), 3.8 (DataTable), 3.9 (rétegek, párbeszédablak) és 3.10 (kulcsok, tranzakció). Ha egy rész nem megy, a hozzá tartozó lecke részletesen elmagyarázza. A megoldásokat lenyitható részekben találod.

3.10 Kapcsolatok és tranzakciók →

Terv: mit tudjon a program?

Egy projektet mindig a követelményekkel kezdünk. Ezeket pontozással ellenőrizheted a végén:

RészKövetelményPont
Adatbázis (15)Három tábla (konyvek, olvasok, kolcsonzesek) elsődleges és idegen kulcsokkal, a kulcsellenőrzés bekapcsolva.10
Mintaadatok, a .db fájl a program mellé másolódik.5
Könyvek (20)Könyv felvétele, módosítása, törlése Konyv osztállyal, KonyvTar adatkezelővel és párbeszédablakkal (3.9 lecke), ellenőrzéssel.14
A könyvet nem lehet törölni, ha van kölcsönzése: érthető üzenet.6
Kölcsönzés (25)Kölcsönzés rögzítése: szabad példány és az olvasó legfeljebb 3 aktív kölcsönzése szabály, tranzakcióban.13
Visszahozás rögzítése; kétszeri visszahozás nem lehetséges.7
A határidő a kölcsönzéskor számolódik (alapból 14 nap).5
Listák (20)Könyvlista a szabad példányok számával (JOIN, GROUP BY).7
Aktuális kölcsönzések listája; a késésben lévők pirosan, a késés napjaival.8
Kereső (cím vagy szerző részletére), biztonságosan.5
Hibakezelés és biztonság (10)Minden SQL paraméterezett; a váratlan hibák naplózása, általános üzenet a felhasználónak.6
Üres vagy hibás bevitel kezelése, megerősítés törlés előtt.4
Kód és szerkezet (10)Rétegekre bontott kód, beszédes nevek, nincs ismétlődő SQL az ablakban.10

Összesen: 100 pont. Mielőtt kódolnál, rajzold le (papíron) az ablak elrendezését, és gondold végig a három tábla kapcsolatát:

konyvek (id, cim, szerzo, ev, peldany)1 — Nkolcsonzesek (id, konyvid, olvasoid, kolcsonzes, hatarido, visszahozva)N — 1olvasok (id, nev, osztaly)

A kolcsonzesek tábla a két másik közötti kapcsolótábla: egy sora azt mondja, hogy ez az olvasó ezt a könyvet ekkor kölcsönözte, eddig kell visszahoznia, és ekkor hozta vissza (a visszahozva üres, amíg a könyv kint van).

📷 Képernyőkép helye

Mit tegyél ide: egy papírra rajzolt terv a főablakról: bal oldalt a könyvek táblázata a keresővel, jobb oldalt az aktuális kölcsönzések, alul a kölcsönzés és visszahozás gombjai és az olvasó választó listája.Javasolt fájlnév: winforms-3-11-terv.webp · Alt szöveg: A könyvtár programjának tervrajza

Az adatbázis

Hozd létre a konyvtar.db adatbázist a DB Browserben, és futtasd az Execute SQL lapon (majd Ctrl+S). A dátumokat ISO szövegként tároljuk (3.7 lecke). Az utolsó rész mintakölcsönzéseket vesz fel, köztük két késésben lévőt (a 2026. október 25-i dátumhoz képest):

SQLSQLite
CREATE TABLE konyvek (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cim TEXT NOT NULL,
szerzo TEXT NOT NULL,
ev INTEGER,
peldany INTEGER NOT NULL DEFAULT 1 CHECK (peldany >= 0)
);

CREATE TABLE olvasok (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nev TEXT NOT NULL,
osztaly TEXT
);

CREATE TABLE kolcsonzesek (
id INTEGER PRIMARY KEY AUTOINCREMENT,
konyvid INTEGER NOT NULL,
olvasoid INTEGER NOT NULL,
kolcsonzes TEXT NOT NULL, -- yyyy-MM-dd
hatarido TEXT NOT NULL, -- yyyy-MM-dd
visszahozva TEXT, -- NULL, amíg a könyv kint van
FOREIGN KEY (konyvid) REFERENCES konyvek (id),
FOREIGN KEY (olvasoid) REFERENCES olvasok (id)
);

INSERT INTO konyvek (cim, szerzo, ev, peldany) VALUES
('Egri csillagok', 'Gárdonyi Géza', 1901, 2),
('A Pál utcai fiúk', 'Molnár Ferenc', 1906, 1),
('Légy jó mindhalálig', 'Móricz Zsigmond', 1920, 1),
('Az arany ember', 'Jókai Mór', 1872, 1),
('Tüskevár', 'Fekete István', 1957, 2);

INSERT INTO olvasok (nev, osztaly) VALUES
('Kovács Anna', '10.A'), ('Nagy Péter', '10.A'), ('Szabó Kata', '10.B'), ('Tóth Bence', '11.A');

INSERT INTO kolcsonzesek (konyvid, olvasoid, kolcsonzes, hatarido, visszahozva) VALUES
(1, 1, '2026-09-20', '2026-10-04', NULL), -- késésben
(2, 2, '2026-10-10', '2026-10-24', NULL), -- késésben
(3, 3, '2026-10-20', '2026-11-03', NULL), -- rendben
(1, 3, '2026-09-01', '2026-09-15', '2026-09-12'), -- visszahozva
(4, 1, '2026-10-22', '2026-11-05', NULL); -- rendben

Ellenőrizd a Foreign Keys pragma beállítását a DB Browserben (Edit Pragmas), majd hozz létre egy új Windows Forms projektet. Telepítsd a System.Data.SQLite csomagot (3.2 lecke), másold a konyvtar.db fájlt a projektbe (Copy if newer), és írd meg a DB.cs osztályt a 3.2, 3.7 és 3.10 leckék szerint: UjKapcsolat() (Foreign Keys=True, a konyvtar.db fájlnévvel és a File.Exists ellenőrzéssel), IsoDatum() és Naploz().

Megoldás megnyitása (próbáld ki előbb önállóan!)
DB.csC#
using System;
using System.Data;
using System.Data.SQLite;
using System.Globalization;
using System.IO;
using System.Windows.Forms;

internal static class DB
{
public static readonly string AdatbazisUt = Path.Combine(Application.StartupPath, "konyvtar.db");

public static SQLiteConnection UjKapcsolat()
{
if (!File.Exists(AdatbazisUt))
{
throw new FileNotFoundException("Nem található az adatbázis: " + AdatbazisUt);
}

return new SQLiteConnection("Data Source=" + AdatbazisUt + ";Version=3;Foreign Keys=True;");
}

public static string IsoDatum(DateTime datum)
{
return datum.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}

public static string LikeVedelem(string szoveg)
{
return szoveg.Replace("^", "^^").Replace("%", "^%").Replace("_", "^_");
}

public static void Naploz(Exception ex)
{
try
{
string utvonal = Path.Combine(Application.StartupPath, "hibanaplo.txt");
File.AppendAllText(utvonal, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " | " + ex.GetType().Name + " | " + ex.Message + Environment.NewLine);
}
catch (Exception)
{
}
}
}

Könyvek kezelése: ugyanaz a szerkezet, mint a 3.9 leckében

A könyvek felvétele, módosítása és törlése a 3.9 leckében látott három rétegben készül: Konyv osztály (adatok és Hiba() ellenőrzés), KonyvTar adatkezelő (Betolt, Mentes, Torles), KonyvUrlap párbeszédablak (ShowDialog, új objektummal tér vissza). Az új elem a törlés védelme: egy könyvet nem szabad törölni, ha van kölcsönzése (az idegen kulcs ezt megakadályozza). A felhasználónak ne a technikai FOREIGN KEY hibát mutasd, hanem érthető üzenetet.

Konyv.Hiba()A cím és a szerző kötelező (legfeljebb 80 karakter), az év 1000 és 2100 között (vagy üres, ha ismeretlen), a példányszám legalább 1.
KonyvTar.Torles(int id)Az InvalidOperationException kivételt dobja, ha a sor nem létezik. Az idegen kulcs megsértése SQLiteException lesz, amelyet az ablak ismer fel.
KonyvUrlapNégy mező (txtCim, txtSzerzo, nudEv, nudPeldany), lblHiba, Mentés (DialogResult: None) és Mégse. Új objektummal tér vissza.
Főablak: Új, Szerkesztés, TörlésA gombok és a dupla kattintás a 3.9 lecke mintáját követik.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A szerkezet a Dolgozo megoldás mintáját követi a konyvek táblára átírva. Az új rész a törlés védelme és az ellenőrzés:

Konyv.cs és Form1.cs (részletek)C#
public class Konyv
{
public int Id { get; set; }
public string Cim { get; set; } = "";
public string Szerzo { get; set; } = "";
public int Ev { get; set; } // 0: ismeretlen
public int Peldany { get; set; } = 1;

public string Hiba()
{
if (Cim.Length == 0 || Szerzo.Length == 0)
{
return "A cím és a szerző kötelező.";
}

if (Cim.Length > 80 || Szerzo.Length > 80)
{
return "A cím és a szerző legfeljebb 80 karakteres lehet.";
}

if (Ev != 0 && (Ev < 1000 || Ev > 2100))
{
return "Az év 1000 és 2100 között legyen (vagy 0, ha ismeretlen).";
}

if (Peldany < 1)
{
return "A példányszám legalább 1.";
}

return "";
}
}

// Form1.cs: a törlés gombjában
try
{
KonyvTar.Torles(id);
Uzenet("A könyv törölve.", false);
}
catch (SQLiteException ex)
{
if (ex.Message.Contains("FOREIGN KEY"))
{
Uzenet("A könyvet nem lehet törölni, mert kölcsönzések tartoznak hozzá.", true);
}
else
{
DB.Naploz(ex);
MessageBox.Show("A törlés most nem sikerült.");
}
}
catch (InvalidOperationException ex)
{
Uzenet(ex.Message, true);
}

A kölcsönzés: szabályok és tranzakció

A kölcsönzés a projekt legfontosabb része. Egy kölcsönzés rögzítése előtt két szabályt kell ellenőrizni, és csak ha mindkettő teljesül, kerülhet az új sor az adatbázisba. Az ellenőrzés és a beszúrás egy tranzakcióban történik, hogy a két lépés egy egységet alkosson:

SzabályHogyan ellenőrzöd?
Van szabad példány a könyvből.peldany értéke > a visszahozva IS NULL kölcsönzések száma az adott könyvre.
Az olvasónál legfeljebb 3 könyv lehet egyszerre.Az olvasó visszahozva IS NULL kölcsönzéseinek száma < 3.
KolcsonzesTar.cs (using System; using System.Data.SQLite;)C#
public static class KolcsonzesTar
{
private const int MaxAktiv = 3;

// egyetlen számot kérdez le, paraméterezve (a tranzakció részeként)
private static int Szam(SQLiteConnection kapcsolat, SQLiteTransaction tranzakcio, string sql, int id)
{
using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat, tranzakcio))
{
parancs.Parameters.AddWithValue("@id", id);
return Convert.ToInt32(parancs.ExecuteScalar());
}
}

public static void Kolcsonoz(int konyvId, int olvasoId, int napok)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteTransaction tranzakcio = kapcsolat.BeginTransaction())
{
int peldany = Szam(kapcsolat, tranzakcio, "SELECT COALESCE(MAX(peldany), 0) FROM konyvek WHERE id = @id", konyvId);
int kint = Szam(kapcsolat, tranzakcio, "SELECT COUNT(*) FROM kolcsonzesek WHERE konyvid = @id AND visszahozva IS NULL", konyvId);
int olvasoKint = Szam(kapcsolat, tranzakcio, "SELECT COUNT(*) FROM kolcsonzesek WHERE olvasoid = @id AND visszahozva IS NULL", olvasoId);

if (peldany == 0)
{
throw new InvalidOperationException("A könyv nem létezik.");
}

if (kint >= peldany)
{
throw new InvalidOperationException("Nincs szabad példány ebből a könyvből.");
}

if (olvasoKint >= MaxAktiv)
{
throw new InvalidOperationException($"Az olvasónál már {MaxAktiv} könyv van, többet nem kölcsönözhet.");
}

using (SQLiteCommand beszur = new SQLiteCommand("INSERT INTO kolcsonzesek (konyvid, olvasoid, kolcsonzes, hatarido) VALUES (@konyv, @olvaso, @ma, @hatarido)", kapcsolat, tranzakcio))
{
beszur.Parameters.AddWithValue("@konyv", konyvId);
beszur.Parameters.AddWithValue("@olvaso", olvasoId);
beszur.Parameters.AddWithValue("@ma", DB.IsoDatum(DateTime.Today));
beszur.Parameters.AddWithValue("@hatarido", DB.IsoDatum(DateTime.Today.AddDays(napok)));
beszur.ExecuteNonQuery();
}

tranzakcio.Commit();
}
}
}

public static void Visszahoz(int kolcsonzesId)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

string sql = "UPDATE kolcsonzesek SET visszahozva = @ma WHERE id = @id AND visszahozva IS NULL";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@ma", DB.IsoDatum(DateTime.Today));
parancs.Parameters.AddWithValue("@id", kolcsonzesId);

if (parancs.ExecuteNonQuery() != 1)
{
throw new InvalidOperationException("Ezt a kölcsönzést már visszahozták, vagy nem létezik.");
}
}
}
}
}
Szam(...) segédmetódusHáromszor kell egy számot lekérdezni, ugyanazzal a szerkezettel. A segédmetódus egyszer írja le, és a kapcsolatot és a tranzakciót paraméterként kapja, így ugyanabban a tranzakcióban dolgozik.
COALESCE(MAX(peldany), 0)Ha nincs ilyen azonosítójú könyv, a MAX eredménye NULL. A COALESCE az első nem üres értéket adja: így 0 lesz, amelyet külön fel tudunk ismerni.
const int MaxAktiv = 3Az olvasónkénti korlát egy névvel ellátott állandó. Egy helyen módosítható, és a kódban érthető.
Szabálysértés = kivételHa egy szabály nem teljesül, az InvalidOperationException kilép a using blokkból, a tranzakció visszavonódik, és az ablak a kivétel szövegét mutatja a felhasználónak. A szabály nem teljesülése várt helyzet, nem programhiba.
Visszahozás: AND visszahozva IS NULLEz a feltétel megakadályozza, hogy egy már visszahozott könyv újra „visszahozásra” kerüljön, és az eredeti dátum felülíródjon. Ha az UPDATE nulla sort érint, a kivétel jelzi.
$"...{MaxAktiv}..."Szöveginterpoláció állandóval is működik.

Jósolj!

A mintaadatokkal a Légy jó mindhalálig könyvből egy példány van, és azt Szabó Kata már kölcsönözte. Mi történik, ha Tóth Bence is kölcsönözni szeretné? És ha Kovács Anna (nála már 2 kint lévő könyv van: Egri csillagok és Az arany ember) a Tüskevár könyvből próbál meg kölcsönözni egy harmadikat, majd egy negyediket?

Jóslat ellenőrzése

Tóth Bence kölcsönzése elutasítódik: Nincs szabad példány ebből a könyvből. (1 példány, 1 kint). Kovács Anna harmadik kölcsönzése sikerül (2 kint lévő könyve volt, a Tüskevár pedig szabad), a negyedik viszont elutasítódik: Az olvasónál már 3 könyv van. Mindkét elutasításnál a tranzakció visszavonódik, az adatbázisban nem történik változás.

📷 Képernyőkép helye

Mit tegyél ide: a főablak, amelyben a kölcsönzés elutasítása után piros üzenet látszik („Nincs szabad példány ebből a könyvből.”).Javasolt fájlnév: winforms-3-11-kolcsonzes.webp · Alt szöveg: A kölcsönzés elutasítása szabálysértés miatt

Listák és késések

A főablakban két lista látszik: a könyvek a szabad példányszámmal, és az aktuális kölcsönzések (a késésben lévők kiemelve). Mindkettő JOIN lekérdezés. Két fontos részlet van bennük:

KonyvTar.cs, KolcsonzesTar.csC#
// KonyvTar.cs: könyvek a szabad példányszámmal, keresővel
public static DataTable Lista(string kereso)
{
string sql = "SELECT b.id, b.cim, b.szerzo, b.ev, b.peldany, b.peldany - COUNT(k.id) AS szabad "
+ "FROM konyvek b "
+ "LEFT JOIN kolcsonzesek k ON k.konyvid = b.id AND k.visszahozva IS NULL "
+ "WHERE b.cim LIKE @minta ESCAPE '^' OR b.szerzo LIKE @minta ESCAPE '^' "
+ "GROUP BY b.id, b.cim, b.szerzo, b.ev, b.peldany "
+ "ORDER BY b.cim";

DataTable tabla = new DataTable();

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@minta", "%" + DB.LikeVedelem(kereso) + "%");

using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(parancs))
{
adapter.Fill(tabla);
}
}
}

return tabla;
}

// KolcsonzesTar.cs: aktuális kölcsönzések a késés jelzésével
public static DataTable Aktualis()
{
string sql = "SELECT k.id, b.cim, o.nev AS olvaso, k.kolcsonzes, k.hatarido, "
+ "CASE WHEN k.hatarido < @ma THEN 'késésben' ELSE 'rendben' END AS allapot "
+ "FROM kolcsonzesek k "
+ "JOIN konyvek b ON b.id = k.konyvid "
+ "JOIN olvasok o ON o.id = k.olvasoid "
+ "WHERE k.visszahozva IS NULL ORDER BY k.hatarido";

DataTable tabla = new DataTable();

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@ma", DB.IsoDatum(DateTime.Today));

using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(parancs))
{
adapter.Fill(tabla);
}
}
}

return tabla;
}
A szűrés az ON-ban, nem a WHERE-benA LEFT JOIN kolcsonzesek k ON k.konyvid = b.id AND k.visszahozva IS NULL feltétel az ON részben van. Ha a visszahozva IS NULL a WHERE-be kerülne, azok a könyvek kiesnének a listából, amelyeknek csak visszahozott kölcsönzésük van: a párosított soruk visszahozva értéke nem NULL, ezért a WHERE kiszűrné őket, pedig minden példányuk szabad. Az ON csak a párosítást szűri, a könyv megmarad (a nem kölcsönzött könyvnél az összes oszlop NULL).
b.peldany - COUNT(k.id) AS szabadA példányszámból levonjuk a kint lévő kölcsönzések számát. A COUNT(k.id) a NULL sorokat (nincs kint lévő kölcsönzés) 0-nak veszi, a COUNT(*) 1-nek venné.
CASE WHEN ... THEN ... ELSE ... ENDFeltételes kifejezés az SQL-ben. Itt a határidő és a mai dátum ISO szövegének összehasonlítása dönti el az állapotot.
A mai dátum paraméterkéntA lekérdezés nem az SQLite saját idejét használja, hanem a program által megadott @ma értéket: így a viselkedés tesztelhető (például egy másik napra állítva).
JOIN a három táblánA kölcsönzések listájához a könyv címe és az olvasó neve is kell: két JOIN kapcsolja a három táblát. (A kapcsolótábla minden sora pontosan egy könyvre és egy olvasóra mutat, ezért itt INNER JOIN elég.)

A késésben lévő sorok piros színezése és a késés napjainak kiírása a táblázat DataBindingComplete eseményében történik (3.8 lecke), a mai és a határidő különbségéből:

Form1.csC#
private void dgvKolcsonzesek_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
int keso = 0;

foreach (DataGridViewRow sor in dgvKolcsonzesek.Rows)
{
if (Convert.ToString(sor.Cells["allapot"].Value) == "késésben")
{
sor.DefaultCellStyle.BackColor = Color.MistyRose;
sor.DefaultCellStyle.ForeColor = Color.Firebrick;
keso = keso + 1;

DateTime hatarido = DateTime.Parse(Convert.ToString(sor.Cells["hatarido"].Value), System.Globalization.CultureInfo.InvariantCulture);
int napok = (DateTime.Today - hatarido).Days;
sor.Cells["allapot"].Value = $"késésben ({napok} nap)";
}
}

lblKesesek.Text = keso == 0 ? "Nincs késésben lévő könyv." : $"Késésben lévő könyvek: {keso}";
}

Jósolj!

2026. október 25-én a mintaadatokkal hány sor kerül az aktuális kölcsönzések listájába, és hány lesz piros? Hány napot késik a A Pál utcai fiúk és az Egri csillagok könyvet kölcsönző olvasó?

Jóslat ellenőrzése

Az öt kölcsönzésből négy van kint (a visszahozott nem látszik): ez 4 sor. Ebből 2 piros: az Egri csillagok határideje 2026-10-04 (21 napos késés), A Pál utcai fiúk határideje 2026-10-24 (1 napos késés). A másik két kölcsönzés határideje novemberben van, ezek rendben vannak. (Más napon indítva a program más számokat mutat: a mai dátum a program indításának napja.)

Kereső és statisztika

A könyvlista keresője a KonyvTar.Lista(kereso) metódust hívja minden gépeléskor (a txtKereso.TextChanged esemény), a betoltve jelzővel védve (3.5 lecke). A statisztikához egy külön kis lekérdezés tartozik: a három legtöbbet kölcsönzött könyv:

SQLSQLite
SELECT b.cim, COUNT(k.id) AS kolcsonzesek
FROM konyvek b
JOIN kolcsonzesek k ON k.konyvid = b.id
GROUP BY b.id, b.cim
ORDER BY kolcsonzesek DESC, b.cim
LIMIT 3;
JOIN (nem LEFT JOIN)Itt csak azokat a könyveket szeretnénk, amelyeket legalább egyszer kölcsönöztek, ezért az INNER JOIN a helyes.
ORDER BY kolcsonzesek DESC, b.cimElőbb a kölcsönzések száma szerint csökkenően, egyenlőség esetén a cím szerint. Így az eredmény mindig ugyanabban a sorrendben jön.
LIMIT 3Az első három sor. A LIMIT csak rendezéssel együtt értelmes (3.5 lecke).
MegjelenítésA három sort egy lblStat felirat mutassa, vagy egy kis ListBox (1. Egri csillagok (2 kölcsönzés)).

Tesztlista és önértékelés

Mielőtt kész állapotúnak nyilvánítod, próbáld ki sorban:

#TesztVárt eredmény
1A program indul, a könyvlista és a kölcsönzések listája megjelenik.Minden sor látszik, a szabad példányok száma helyes.
2Új könyv felvétele, majd módosítása.A lista frissül, a módosított sor kijelölve marad.
3Könyv felvétele üres címmel, majd Mégse.Hibaüzenet; a Mégse után nem történik semmi.
4Kölcsönzés szabad példányra.Az új sor megjelenik a kölcsönzések között, a szabad példányszám eggyel csökken, a határidő 14 nap.
5Kölcsönzés, ha nincs szabad példány.Érthető üzenet, nem változik semmi.
6Olvasó negyedik kölcsönzése.Elutasítás a 3 könyves korlát miatt.
7Visszahozás.A sor eltűnik az aktuális listából, a szabad példányszám nő.
8Ugyanannak a kölcsönzésnek a második visszahozása (két ablakból, vagy a DB Browserben előkészítve).Érthető üzenet, a visszahozás dátuma nem íródik felül.
9A késésben lévő kölcsönzések.Pirosak, a késés napjaival.
10Kölcsönzött könyv törlése.Érthető üzenet, a könyv megmarad.
11Kölcsönzés nélküli könyv törlése megerősítéssel.Megerősítés után eltűnik.
12Keresés: ékezetes, nagybetűs, %, _ és ' jellel.Hiba nélkül, a speciális karakterek szó szerint értendők.
13Az adatbázisfájl átnevezése, majd indítás.Érthető hibaüzenet, a naplóban a részletek.
14A program újraindítása.Az adatok megmaradtak.

Önértékelés

Működés (kb. 60 pont)A tesztlista mind a 14 pontja teljesül.
Biztonság (kb. 10 pont)Minden SQL paraméterezett (az ORDER BY csak rögzített szöveg); nincs belső hibaüzenet a felhasználónak; a napló írja a részleteket.
Szerkezet (kb. 20 pont)Az ablakban nincs SQL; az üzleti szabályok a Hiba() és az adatkezelő osztályokban vannak; nincs kétszer megírt kód.
Használhatóság (kb. 10 pont)Érthető feliratok, megerősítés a törlés előtt, a Mégse mindig biztonságos, a kijelölés megmarad frissítés után.

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. Eltűnnek a könyvek, amelyeket már visszahoztak

FROM konyvek b LEFT JOIN kolcsonzesek k ON k.konyvid = b.id
WHERE k.visszahozva IS NULL
Hiba és javítás

A WHERE a LEFT JOIN után szűr. Az a könyv, amelynek csak visszahozott kölcsönzése van, csak olyan sorral párosul, ahol a visszahozva nem NULL, ezért a WHERE kiszűri, és a könyv teljesen eltűnik a listából, pedig minden példánya szabad. A szűrést a JOIN ... ON ... AND k.visszahozva IS NULL feltételbe tedd: ott csak a párosítást korlátozza, a könyv sora megmarad.

2. Negatív szabad példányszám

Egy könyvnél a szabad példányszám 0 helyett 1 vagy negatív szám.

Ok és javítás

Ha COUNT(*)-ot használsz, a LEFT JOIN által adott egyetlen NULL sort is 1-nek számolja. A COUNT(k.id) a NULL értéket kihagyja. Másik ok lehet, hogy a kölcsönzés ellenőrzése tranzakció nélkül történt, és két kölcsönzés egyszerre túllépte a példányszámot.

3. A késés rosszul számolódik

A határidő 2026-10-04, de a program nem tekinti késésben lévőnek a 2026-10-24 napot.

Ok és javítás

Valószínűleg nem ISO formátumban tárolod a dátumot (például 04.10.2026), vagy nem ISO formátumban adod át a mai dátumot. A szöveges összehasonlítás csak yyyy-MM-dd alakban egyezik az időrenddel (3.7 lecke). Ellenőrizd a DB.IsoDatum használatát, és hogy a határidő is ISO alakban került az adatbázisba.

4. A visszahozás dátuma felülíródik

UPDATE kolcsonzesek SET visszahozva = @ma WHERE id = @id
Hiba és javítás

Hiányzik az AND visszahozva IS NULL feltétel, ezért egy már visszahozott kölcsönzés dátuma is újra felülíródik, és a program nem veszi észre a kétszeri visszahozást. A feltétellel nulla sort érint az utasítás, és a metódus kivétellel jelzi.

5. A kölcsönzés fele megmaradt

Hiba után az adatbázisban egy kölcsönzés félig bekerült adatokkal maradt.

Ok és javítás

Az ellenőrzés és a beszúrás nem tranzakcióban történt, vagy a Commit kimaradt, vagy a parancsok nem a tranzakcióhoz kötött kapcsolaton futottak (3.10 lecke). Minden összetartozó lépést egyetlen kapcsolaton, egyetlen tranzakcióban futtass, és a végén hívd a Commit metódust.

6. A könyv törlésekor technikai hibaüzenet

A felhasználó a FOREIGN KEY constraint failed üzenetet látja.

Ok és javítás

Az SQLiteException üzenetét közvetlenül kiírtad. Ismerd fel a kulcsszót (ex.Message.Contains("FOREIGN KEY")), és adj érthető üzenetet. A többi hibát naplózd, és általános szöveget mutass.

Három önálló bővítés

Ha kész a projekt, fejleszd tovább. Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. Meghosszabbítás

Egy Meghosszabbítás gomb 7 nappal hosszabbítsa a kijelölt kölcsönzés határidejét, de csak egyszer, és csak akkor, ha még nincs késésben.

Egy lehetséges megoldás

Az „egyszer” szabályhoz új oszlop kell a táblába (meghosszabbitva, 0 vagy 1). Az ALTER TABLE utasítás oszlopot ad egy meglévő táblához. Az UPDATE feltételei adják a szabályt: nincs visszahozva, még nem volt meghosszabbítva, és a határidő nem múlt el. Az érintett sorok száma jelzi, teljesültek-e a szabályok.

SQL és KolcsonzesTar.csC#
-- egyszer, a DB Browserben:
ALTER TABLE kolcsonzesek ADD COLUMN meghosszabbitva INTEGER NOT NULL DEFAULT 0;

// KolcsonzesTar.cs
public static void Meghosszabbit(int kolcsonzesId, string ujHatarido)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

string sql = "UPDATE kolcsonzesek SET hatarido = @uj, meghosszabbitva = 1 "
+ "WHERE id = @id AND visszahozva IS NULL AND meghosszabbitva = 0 AND hatarido >= @ma";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@uj", ujHatarido);
parancs.Parameters.AddWithValue("@id", kolcsonzesId);
parancs.Parameters.AddWithValue("@ma", DB.IsoDatum(DateTime.Today));

if (parancs.ExecuteNonQuery() != 1)
{
throw new InvalidOperationException("A kölcsönzés nem hosszabbítható (már volt hosszabbítva, késésben van, vagy visszahozták).");
}
}
}
}

2. Olvasó története

Egy olvasó kiválasztásakor egy külön táblázat mutassa az összes eddigi kölcsönzését (visszahozottakat is): könyv címe, kölcsönzés, határidő, visszahozás dátuma (vagy kint van).

Egy lehetséges megoldás

A lekérdezés az olvasó azonosítójára szűr, és COALESCE segítségével szöveget ad a hiányzó visszahozási dátum helyett.

SQLSQLite
SELECT b.cim, k.kolcsonzes, k.hatarido,
COALESCE(k.visszahozva, 'kint van') AS visszahozva
FROM kolcsonzesek k
JOIN konyvek b ON b.id = k.konyvid
WHERE k.olvasoid = @olvaso
ORDER BY k.kolcsonzes DESC;

3. Késések exportálása

Egy gomb mentse a késésben lévő kölcsönzéseket CSV fájlba (olvasó neve, könyv címe, határidő, késés napjai), hogy a könyvtáros kinyomtathassa.

Egy lehetséges megoldás

Az Aktualis() eredményéből a DataTable.Select("allapot = 'késésben'") szűri a késéseket (a 3.8 lecke szerint). A StreamWriter soronként írja a fájlt, pontosvesszővel elválasztva. Az idézőjelet és a pontosvesszőt tartalmazó értékeket (például egy cím) idézőjelek közé kell tenni, ezt a példa egyszerűsítve kezeli.

Form1.csC#
DataTable tabla = KolcsonzesTar.Aktualis();
DataRow[] kesok = tabla.Select("allapot = 'késésben'");

using (System.IO.StreamWriter iro = new System.IO.StreamWriter(sfdExport.FileName))
{
iro.WriteLine("olvaso;konyv;hatarido;keses_napok");

foreach (DataRow sor in kesok)
{
DateTime hatarido = DateTime.Parse(sor["hatarido"].ToString(), System.Globalization.CultureInfo.InvariantCulture);
int napok = (DateTime.Today - hatarido).Days;

iro.WriteLine(sor["olvaso"] + ";" + sor["cim"] + ";" + sor["hatarido"] + ";" + napok);
}
}

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Az első négy a DB Browser Execute SQL lapján oldható meg a konyvtar.db adatbázissal. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Hány könyv van kint?

Add meg, hány könyv van jelenleg kint (nincs visszahozva).

Megoldás megnyitása

A hiányzó értéket az IS NULL vizsgálja, nem az = NULL (3.5 lecke).

SQLSQLite
SELECT COUNT(*) AS kint_levo FROM kolcsonzesek WHERE visszahozva IS NULL;

2. A késésben lévők

Írd ki a késésben lévő kölcsönzések olvasóját, a könyv címét és a határidőt, ha ma 2026. november 1. (a dátum legyen a lekérdezésben).

Megoldás megnyitása

Az ISO szöveges dátumok összehasonlítása időrendben működik. Ugyanezt a program paraméterként kapja a mai dátummal.

SQLSQLite
SELECT o.nev AS olvaso, b.cim, k.hatarido
FROM kolcsonzesek k
JOIN konyvek b ON b.id = k.konyvid
JOIN olvasok o ON o.id = k.olvasoid
WHERE k.visszahozva IS NULL AND k.hatarido < '2026-11-01'
ORDER BY k.hatarido;

Közepes szint

3. Olvasók és kölcsönzéseik száma

Írd ki minden olvasó nevét és azt, hány könyvet kölcsönzött összesen (azokat is, akik még nem kölcsönöztek: nulla).

Megoldás megnyitása

A LEFT JOIN megtartja a kölcsönzés nélküli olvasókat, a COUNT(k.id) pedig a NULL sorokat nem számolja (nulla lesz).

SQLSQLite
SELECT o.nev, COUNT(k.id) AS kolcsonzesek
FROM olvasok o
LEFT JOIN kolcsonzesek k ON k.olvasoid = o.id
GROUP BY o.id, o.nev
ORDER BY kolcsonzesek DESC, o.nev;

4. Soha nem kölcsönzött könyvek

Add meg azokat a könyveket, amelyeket még senki nem kölcsönzött ki.

Megoldás megnyitása

A LEFT JOIN után azokat a sorokat tartjuk meg, ahol a kölcsönzés azonosítója NULL (nem találtunk párt). A NOT EXISTS ugyanezt al-lekérdezéssel fejezi ki.

SQLSQLite
SELECT b.cim
FROM konyvek b
LEFT JOIN kolcsonzesek k ON k.konyvid = b.id
WHERE k.id IS NULL;

SELECT cim FROM konyvek b
WHERE NOT EXISTS (SELECT 1 FROM kolcsonzesek k WHERE k.konyvid = b.id);

Kihívás szint

5. Állítható kölcsönzési idő és korlát

A 14 napos időtartam és a 3 könyves korlát legyen beállítható egy nudNapok és egy nudMax mezővel, anélkül, hogy a KolcsonzesTar osztály ismerné az ablakot. Hogyan adod át az értékeket?

Megoldás megnyitása

A Kolcsonoz metódus paramétereket kap (napok, maxAktiv). Az MaxAktiv állandó helyett paraméter lesz. Az ablak olvassa a két mezőt, és adja át. Így az adatkezelő nem függ az ablaktól.

KolcsonzesTar.cs és Form1.csC#
// KolcsonzesTar.cs
public static void Kolcsonoz(int konyvId, int olvasoId, int napok, int maxAktiv)
{
// ... a korlát ellenőrzése: if (olvasoKint >= maxAktiv) ...
}

// Form1.cs
KolcsonzesTar.Kolcsonoz(konyvId, olvasoId, (int)nudNapok.Value, (int)nudMax.Value);

6. Biztonsági mentés

Egy Mentés gomb másolja az adatbázis fájlját a felhasználó által megadott mappába, az időponttal ellátott néven (konyvtar_20261025_1930.db). Mire kell vigyázni?

Megoldás megnyitása

A File.Copy a fájlt másolja. A másolás előtt nem szabad nyitva lennie az adatbázis-kapcsolatnak, ezért a using blokkok használata fontos (a program nem tart folyamatosan nyitott kapcsolatot). A mappát egy FolderBrowserDialog kéri. A másolás hibáit (nincs jogosultság, nincs hely) try–catch kezeli.

Form1.csC#
private void btnMentes_Click(object sender, EventArgs e)
{
if (fbdMappa.ShowDialog() != DialogResult.OK)
{
return;
}

try
{
string nev = "konyvtar_" + DateTime.Now.ToString("yyyyMMdd_HHmm") + ".db";
string cel = System.IO.Path.Combine(fbdMappa.SelectedPath, nev);

System.IO.File.Copy(DB.AdatbazisUt, cel);
Uzenet("A biztonsági mentés elkészült: " + nev, false);
}
catch (Exception ex)
{
DB.Naploz(ex);
Uzenet("A mentés nem sikerült.", true);
}
}

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért van szükség kapcsolótáblára a könyvek és az olvasók között?

Mert egy olvasó több könyvet kölcsönözhet, és egy könyvet több olvasó is kölcsönöz az idők során (sok-a-többhöz kapcsolat). A kapcsolótábla minden sora egy kölcsönzést ír le.

2. Mit jelent, hogy a visszahozva oszlop NULL?

A könyv még kint van: nincs visszahozási dátum. A kint lévő kölcsönzéseket az IS NULL feltétel szűri.

3. Miért tranzakcióban ellenőrizzük a szabályokat és szúrjuk be a kölcsönzést?

Hogy az ellenőrzés és a beszúrás egy oszthatatlan egységet alkosson. Szabálysértés vagy hiba esetén a tranzakció visszavonódik, és az adatbázisban nem marad félkész állapot.

4. Miért kerül a szűrés a LEFT JOIN ON részébe?

Hogy a bal oldali tábla sorai (a nem kölcsönzött könyvek) megmaradjanak. A WHERE a párosítás után szűrne, és a párt nem találó sorokat is kiszűrné.

5. Mi a különbség a COUNT(*) és a COUNT(k.id) között LEFT JOIN után?

A COUNT(*) a pár nélküli sort is 1-nek számolja, a COUNT(k.id) csak a létező párokat (a NULL értéket kihagyja).

6. Miért kell a mai dátumot paraméterként átadni?

Hogy a viselkedés tesztelhető legyen, és a program kontrollálja az időt. Az ISO formátum biztosítja a helyes szöveges összehasonlítást.

7. Mit jelent az, hogy az ablakban nincs SQL?

Az SQL az adatkezelő osztályokban (KonyvTar, KolcsonzesTar) van, az ablak csak hívja őket. Így egy változtatás egy helyen történik, és az ablak a felületre koncentrál.

Mit tudsz már? (a 3. fejezet végén)

  • Adatbázist tervezel kulcsokkal és kapcsolótáblával.
  • Biztonságos, paraméteres lekérdezéseket és JOIN-okat írsz.
  • Rétegekre bontott, adatbázisos Windows Forms alkalmazást építesz.
  • Tranzakcióval védesz összetartozó műveleteket, és szabályokat érvényesítesz.
  • Teszteled és értékeled a saját projektedet.

Következő: önálló feladatok

A 3. fejezet kész. A következő oldalakon önálló, időre megoldható feladatok várnak a tanultakból: vezérlőelemek, szövegszerkesztő és adatbázis.