Grafikus C# – Windows Forms tanfolyam › 3.10 lecke Windows Forms · 3.10 lecke · több tábla Eddig minden adatot egyetlen táblában tároltunk. A valódi adatbázisok több, egymással kapcsolatban álló táblából állnak: egy osztályhoz sok diák tartozik, egy diák egy osztályba jár. Ebben a leckében a kapcsolatot idegen kulccsal építed fel, a táblákat a Kapcsolatok, elsődleges és idegen kulcsok, egyszerű tranzakciók
JOIN kapcsolja össze a lekérdezésben, és megtanulod, hogyan lehet több műveletet tranzakcióban végrehajtani, hogy vagy mind sikerüljön, vagy semmi.
A lecke végére
- érted az elsődleges kulcs és az idegen kulcs szerepét, és az egy-a-többhöz kapcsolatot;
- két táblát hozol létre
FOREIGN KEYkapcsolattal, és bekapcsolod az ellenőrzését SQLite-ban (Foreign Keys=True); INNER JOINésLEFT JOINlekérdezéssel összekapcsolt adatokat kérdezel le;- a
ComboBoxDataSource,DisplayMemberésValueMembertulajdonságával a felhasználó nevet lát, a program azonosítót kap; - tranzakciót használsz (
BeginTransaction,Commit,Rollback), és érted, mikor szükséges; - felismered a leggyakoribb hibákat (kikapcsolt kulcsellenőrzés, elfelejtett
Commit, kétértelmű oszlopnév).
Mielőtt elkezded
Ehhez a leckéhez kell a 3.5 lecke (WHERE, GROUP BY), a 3.6 lecke (INSERT, ExecuteNonQuery, LastInsertRowId), a 3.7 lecke (paraméterek) és a 3.8 lecke (DataTable). Az egész lecke egy új, gyakorló adatbázissal dolgozik, ezért a korábbi adataid nem sérülnek.
Miért kell több tábla? Elsődleges és idegen kulcs
Képzeld el, hogy egyetlen táblában tárolod a diákokat és az osztályaikat is: minden diák sorában ott áll, hogy 10.A. Ha az osztály nevét át kell írni (például 10.A helyett 10.A (emelt)), az összes diák sorát módosítani kell, és ha egy sorban elírod, az adatok ellentmondásossá válnak. A megoldás: az osztályok külön táblába kerülnek, és a diák csak az osztály azonosítóját tárolja:
| Fogalom | Jelentése | Példa |
|---|---|---|
Elsődleges kulcs (PRIMARY KEY) | A tábla minden sorát egyedileg azonosító oszlop. | osztalyok.id |
Idegen kulcs (FOREIGN KEY) | Egy oszlop, amely egy másik tábla elsődleges kulcsára hivatkozik. | diakok.osztalyid → osztalyok.id |
| Egy-a-többhöz kapcsolat | Az egyik tábla egy sorához a másik tábla több sora tartozhat. | Egy osztályhoz több diák. |
| Hivatkozási épség | Az adatbázis nem engedi, hogy a hivatkozás nem létező sorra mutasson. | Nem vehető fel diák a 99-es azonosítójú osztályba, ha nincs ilyen. |
Hozz létre egy új, gyakorló adatbázist (például iskola.db) a DB Browserben, és futtasd az Execute SQL lapon (majd Ctrl+S):
CREATE TABLE osztalyok (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nev TEXT NOT NULL UNIQUE
);
CREATE TABLE diakok (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nev TEXT NOT NULL,
osztalyid INTEGER,
FOREIGN KEY (osztalyid) REFERENCES osztalyok (id)
);
INSERT INTO osztalyok (nev) VALUES ('10.A'), ('10.B'), ('11.A');
INSERT INTO diakok (nev, osztalyid) VALUES ('Kovács Anna', 1);
INSERT INTO diakok (nev, osztalyid) VALUES ('Nagy Péter', 1);
INSERT INTO diakok (nev, osztalyid) VALUES ('Szabó Kata', 2);
INSERT INTO diakok (nev, osztalyid) VALUES ('Tóth Bence', 2);
INSERT INTO diakok (nev, osztalyid) VALUES ('Kiss Márta', 2);
INSERT INTO diakok (nev, osztalyid) VALUES ('Varga Dávid', 3);
INSERT INTO diakok (nev, osztalyid) VALUES ('Fekete Dóra', NULL); -- még nincs osztályadiakok.osztalyid értéke csak olyan szám lehet, amely az osztalyok tábla id oszlopában szerepel (vagy NULL).NOT NULL, egy diáknak lehet, hogy még nincs osztálya. (Fekete Dóra.)Az SQLite alapból nem ellenőrzi az idegen kulcsot!
Az SQLite-ban a FOREIGN KEY ellenőrzése kapcsolatonként kapcsolható be, és alapból ki van kapcsolva. A DB Browserben az Edit Pragmas lapon a Foreign Keys pipa megfelelő beállításáról győződj meg (általában be van kapcsolva). A C# programban a kapcsolati szövegbe kell írni: Foreign Keys=True;. Enélkül a program nem létező osztályazonosítót is elfogad, és a hivatkozási épség csendben megsérül.
return new SQLiteConnection("Data Source=" + AdatbazisUt + ";Version=3;Foreign Keys=True;");Jósolj!
Bekapcsolt kulcsellenőrzés mellett a INSERT INTO diakok (nev, osztalyid) VALUES ('Új Diák', 99) utasítást futtatod, de nincs 99-es osztály. Mi történik? És az DELETE FROM osztalyok WHERE id = 2 utasítással, amikor a 10.B osztályban még vannak diákok?
Jóslat ellenőrzése
Mindkettő hibát ad: FOREIGN KEY constraint failed. Az első azért, mert a 99-es osztály nem létezik (a hivatkozás érvénytelen lenne). A második azért, mert a 10.B osztályra még diákok hivatkoznak, és törlésével árva (gazdátlan) hivatkozások keletkeznének. Az adatbázis így védi az adatok épségét. (A viselkedést az ON DELETE kiegészítéssel lehet módosítani: SET NULL ilyenkor a diákok osztályát törli, CASCADE a diákokat is törli.)
JOIN: a két tábla összekapcsolása a lekérdezésben
A diák táblában csak az osztály azonosítója szerepel (1, 2, 3). Olvasható listához az osztály nevét kell mellétenni. Ezt a JOIN végzi: a két tábla sorait az ON feltétel szerint párosítja:
-- csak azok a diákok, akiknek van osztályuk
SELECT d.id, d.nev AS diak, o.nev AS osztaly
FROM diakok d
INNER JOIN osztalyok o ON d.osztalyid = o.id
ORDER BY o.nev, d.nev;
-- minden diák, osztály nélkül is (az osztaly oszlop ilyenkor NULL)
SELECT d.id, d.nev AS diak, o.nev AS osztaly
FROM diakok d
LEFT JOIN osztalyok o ON d.osztalyid = o.id
ORDER BY o.nev, d.nev;
-- osztályonként a létszám, az üres osztályokat is mutatva
SELECT o.nev AS osztaly, COUNT(d.id) AS letszam
FROM osztalyok o
LEFT JOIN diakok d ON d.osztalyid = o.id
GROUP BY o.id, o.nev
ORDER BY o.nev;d.nev, o.nev.FROM után álló) minden sorát megtartja, a hiányzó párokat NULL értékkel tölti ki.nev oszlop: az alias nélkül kétértelmű oszlopnév (ambiguous column name) hibát kapnál, és az eredmény két nev oszlopa is összekeveredne.NULL értéket kihagyja). A COUNT(*) az üres osztályra is 1-et adna, mert a LEFT JOIN egy sort hagy meg hozzá.Jósolj!
Hány sort ad az INNER JOIN, és hányat a LEFT JOIN a fenti adatokkal? Szerepel-e Fekete Dóra mindkettőben? Mi az osztaly oszlop az ő sorában?
Jóslat ellenőrzése
Az INNER JOIN 6 sort ad (azok a diákok, akiknek van osztályuk), a LEFT JOIN 7 sort. Fekete Dóra csak a LEFT JOIN eredményében szerepel, és az osztaly oszlop nála NULL (üres). Ha az összekapcsolt listában mindenkit látni szeretnél, LEFT JOIN kell.
Az ON nélküli JOIN mindent mindennel párosít
Ha az ON feltétel kimarad (vagy hibás), a két tábla minden sora minden sorral párosul (keresztszorzat): 7 diák és 3 osztály esetén 21 sor, nagy táblákon pedig hatalmas eredmény. Mindig ellenőrizd, hogy az ON a megfelelő kulcsokat kapcsolja össze.
C#: JOIN a táblázatban, osztály választása a ComboBoxból
Előkészület: az új adatbázis a program mellé kerül (Copy if newer, 3.2 lecke), a DB.cs osztályban pedig az AdatbazisUt fájlneve legyen iskola.db (belepes.db helyett). Ezt a leckét egy új projektben is megcsinálhatod, hogy a korábbi munkáidhoz ne kelljen hozzányúlni.
A JOIN-nal összekapcsolt lekérdezés eredménye is egy egyszerű DataTable, amelyet a DataGridView megjelenít. Az adatkezelő osztály (DB) három új metódust kap. A legfontosabb szabály itt is: a felhasználó választását (az osztály azonosítóját) paraméterként adjuk át:
public static DataTable Osztalyok()
{
DataTable tabla = new DataTable();
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteDataAdapter adapter = new SQLiteDataAdapter("SELECT id, nev FROM osztalyok ORDER BY nev", kapcsolat))
{
adapter.Fill(tabla);
}
}
return tabla;
}
// osztalyId == 0: minden diák; egyébként csak a választott osztály diákjai
public static DataTable Diakok(int osztalyId)
{
string sql = "SELECT d.id, d.nev AS diak, o.nev AS osztaly "
+ "FROM diakok d LEFT JOIN osztalyok o ON d.osztalyid = o.id "
+ "WHERE (@oszt = 0 OR d.osztalyid = @oszt) "
+ "ORDER BY o.nev, d.nev";
DataTable tabla = new DataTable();
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@oszt", osztalyId);
using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(parancs))
{
adapter.Fill(tabla);
}
}
}
return tabla;
}
// osztalyId == 0: a diák osztály nélkül kerül be (NULL)
public static long DiakFelvetel(string nev, int osztalyId)
{
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteCommand parancs = new SQLiteCommand("INSERT INTO diakok (nev, osztalyid) VALUES (@nev, @oszt)", kapcsolat))
{
parancs.Parameters.AddWithValue("@nev", nev);
parancs.Parameters.AddWithValue("@oszt", osztalyId == 0 ? (object)DBNull.Value : osztalyId);
parancs.ExecuteNonQuery();
}
return kapcsolat.LastInsertRowId;
}
}Az ablakban a ComboBox az osztály nevét mutatja, de a program az azonosítót szeretné megkapni. Ezt a DataSource, a DisplayMember és a ValueMember hármas oldja meg: a lista egy DataTable-ből tölti fel magát, a DisplayMember a megjelenített oszlop neve, a ValueMember az értéket adó oszlop:
private bool betoltve = false;
private void Form1_Load(object sender, EventArgs e)
{
try
{
// szűrő: (összes) + az osztályok
DataTable osztalyok = DB.Osztalyok();
DataRow osszes = osztalyok.NewRow();
osszes["id"] = 0;
osszes["nev"] = "(összes)";
osztalyok.Rows.InsertAt(osszes, 0);
cmbSzuro.DisplayMember = "nev"; // előbb a tagok, utána az adatforrás
cmbSzuro.ValueMember = "id";
cmbSzuro.DataSource = osztalyok;
// az új diákhoz tartozó lista: külön DataTable, hogy a két lista független legyen
DataTable osztalyok2 = DB.Osztalyok();
DataRow nincs = osztalyok2.NewRow();
nincs["id"] = 0;
nincs["nev"] = "(még nincs osztálya)";
osztalyok2.Rows.InsertAt(nincs, 0);
cmbUjOsztaly.DisplayMember = "nev";
cmbUjOsztaly.ValueMember = "id";
cmbUjOsztaly.DataSource = osztalyok2;
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("Az osztályok betöltése nem sikerült.");
return;
}
betoltve = true;
Frissit();
}
private void Frissit()
{
if (!betoltve)
{
return;
}
try
{
int osztalyId = Convert.ToInt32(cmbSzuro.SelectedValue);
dgvDiakok.DataSource = DB.Diakok(osztalyId);
dgvDiakok.Columns["id"].Visible = false;
dgvDiakok.Columns["diak"].HeaderText = "Diák";
dgvDiakok.Columns["osztaly"].HeaderText = "Osztály";
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A lista betöltése nem sikerült.");
}
}
private void cmbSzuro_SelectedIndexChanged(object sender, EventArgs e)
{
Frissit();
}
private void btnDiakFelvetel_Click(object sender, EventArgs e)
{
string nev = txtDiakNev.Text.Trim();
if (nev.Length == 0)
{
lblUzenet.Text = "Add meg a diák nevét!";
return;
}
try
{
DB.DiakFelvetel(nev, Convert.ToInt32(cmbUjOsztaly.SelectedValue));
txtDiakNev.Clear();
lblUzenet.Text = "A diák felvéve.";
Frissit();
}
catch (Exception ex)
{
DB.Naploz(ex);
lblUzenet.Text = "A felvétel nem sikerült.";
}
}DisplayMember az oszlop, amelynek értékét a felhasználó látja (nev), a ValueMember az, amelyet a program kap (id). A SelectedValue a kiválasztott elem azonosítója.DataSource-ot állítod be előbb, a lista már az első feltöltéskor kiválthatja a SelectedIndexChanged eseményt, amikor a ValueMember még nincs beállítva. Ezért előbb a tagok, utána az adatforrás.ComboBox ugyanazt a DataTable-t használná, kijelölésük összefonódna (a DataTable közös aktuális elemet jegyez meg). Külön táblából független marad.Jósolj!
A cmbUjOsztaly listában a (még nincs osztálya) elemet választod, és felveszel egy diákot. Mi kerül az osztalyid oszlopba? Melyik sorban fog megjelenni a táblázatban, ha a szűrő (összes)?
Jóslat ellenőrzése
Az azonosító 0, ezért a DiakFelvetel DBNull.Value értéket ír be (NULL): a kulcsellenőrzés ezt elfogadja (a NULL nem hivatkozik semmire). A LEFT JOIN miatt a diák az (összes) szűrőnél az osztály nélküli sorok között (üres osztály oszloppal) látszik. Ha a 0 azonosítót közvetlenül beírtad volna, a kulcsellenőrzés (nincs 0 azonosítójú osztály) FOREIGN KEY constraint failed hibát adna.
Mit tegyél ide: a futó program: felül az osztályszűrő (10.B kiválasztva), alatta a táblázat három diákkal, lent a diákfelvétel mezője és osztályválasztója.Javasolt fájlnév: winforms-3-10-nevsor.webp · Alt szöveg: A diáklista osztályszűrővel és felvételi űrlappal
Tranzakciók: vagy mind, vagy semmi
Egy új osztály felvételekor két dolgot kell tenni: létrehozni az osztályt, majd felvenni a diákjait. Mi történik, ha az osztály létrejön, de a harmadik diák felvételénél hiba keletkezik? Egy félig kész osztály marad az adatbázisban. A tranzakció ennek a megoldása: több műveletet egyetlen, oszthatatlan egységként hajt végre. Ha mind sikerül, a Commit véglegesíti őket; ha bármelyik elbukik, a Rollback az összeset visszavonja. A kód:
public static int OsztalyDiakokkal(string osztalyNev, string[] diakNevek)
{
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteTransaction tranzakcio = kapcsolat.BeginTransaction())
{
long osztalyId;
using (SQLiteCommand ujOsztaly = new SQLiteCommand("INSERT INTO osztalyok (nev) VALUES (@nev)", kapcsolat, tranzakcio))
{
ujOsztaly.Parameters.AddWithValue("@nev", osztalyNev);
ujOsztaly.ExecuteNonQuery();
osztalyId = kapcsolat.LastInsertRowId;
}
using (SQLiteCommand ujDiak = new SQLiteCommand("INSERT INTO diakok (nev, osztalyid) VALUES (@nev, @oszt)", kapcsolat, tranzakcio))
{
ujDiak.Parameters.Add("@nev", DbType.String);
ujDiak.Parameters.AddWithValue("@oszt", osztalyId);
foreach (string nev in diakNevek)
{
ujDiak.Parameters["@nev"].Value = nev;
ujDiak.ExecuteNonQuery();
}
}
tranzakcio.Commit(); // csak ide jut el, ha minden sikerült
return (int)osztalyId;
} // commit nélkül a using visszavonja (Rollback) a tranzakciót
}
}Commit előtt kivétel keletkezik, a blokk elhagyásakor a SQLiteTransaction felszabadítása automatikusan visszavonja a tranzakciót. Kifejezetten is meghívható: tranzakcio.Rollback().Commit előtt rendelkezésre áll, így a diákok hivatkozhatnak rá.Jósolj!
A OsztalyDiakokkal("10.A", ...) hívást futtatod, de a 10.A osztály már létezik (az osztalyok.nev egyedi). Mi történik? Marad-e valami az adatbázisban a diákokból?
Jóslat ellenőrzése
Az első INSERT UNIQUE constraint failed hibával elbukik, a kivétel kilép a using blokkból, a tranzakció visszavonódik, és semmi nem kerül az adatbázisba: se osztály, se diák. A hívó try–catch blokkja kezeli a kivételt (például: Ez az osztály már létezik). Hasonló lenne, ha a harmadik diák felvétele bukna el: az osztály és az első két diák is visszavonódna.
A tranzakció gyorsít is
Tranzakció nélkül az SQLite minden egyes INSERT után külön, biztonságosan a lemezre ír, ami lassú. Sok sor felvételekor egyetlen tranzakcióba foglalva a művelet sokszorosan gyorsabb. Próbáld ki: mérd meg az időt a Stopwatch osztállyal (System.Diagnostics), először tranzakció nélkül, majd tranzakcióval, például 300 sor felvételével egy gyakorló táblába. A pontos különbség a géptől függ, de jól látható.
Egy tranzakció, egy kapcsolat
A tranzakció alatt az ugyanazon a kapcsolaton dolgozz. Ha közben egy másik kapcsolat (például egy másik DB metódus, amely újat nyit) ugyanabba az adatbázisba próbál írni, az SQLite database is locked hibát adhat. A tranzakcióban futó kódból tehát ne hívj olyan metódust, amely saját kapcsolatot nyit.
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. Nem létező osztályba is bekerül a diák
A program a 99-es osztályba is felvesz egy diákot, és nincs hibaüzenet. Az SQLite alapból nem ellenőrzi az idegen kulcsot. Add a kapcsolati szöveghez a Ok és javítás
Foreign Keys=True; beállítást. Ellenőrizd a DB Browserben is az Edit Pragmas lapon a Foreign Keys beállítást, és a tesztet egy nem létező azonosítóval ismételd meg.
2. FOREIGN KEY constraint failed törléskor
DELETE FROM osztalyok WHERE id = 2;
-- SQLiteException: FOREIGN KEY constraint failedHiba és javítás
Ez nem hiba, hanem védelem: a 10.B osztályra még diákok hivatkoznak. A felhasználónak érthető üzenetet kell adni (Az osztályt nem lehet törölni, amíg diákjai vannak). Megoldások: előbb áthelyezed vagy törlöd a diákokat, vagy a tábla létrehozásakor megadod az ON DELETE SET NULL (a diákok osztálya törlődik) illetve ON DELETE CASCADE (a diákok is törlődnek, ez veszélyes!) beállítást.
3. Elfelejtett Commit
using (SQLiteTransaction tranzakcio = kapcsolat.BeginTransaction())
{
// ... INSERT utasítások ...
// tranzakcio.Commit(); kimaradt
}Hiba és javítás
A Commit nélkül a blokk elhagyásakor a tranzakció visszavonódik, és az összes INSERT elvész, hibaüzenet nélkül. Mindig a sikeres befejezés végén hívd a Commit metódust.
4. A parancs nincs a tranzakcióhoz kötve
SQLiteCommand parancs = new SQLiteCommand(sql, masikKapcsolat); // másik kapcsolat!Hiba és javítás
A tranzakció egy kapcsolathoz tartozik. Ha a parancsot másik kapcsolattal hozod létre, nem része a tranzakciónak (a rollback nem érinti), és database is locked hibát is kaphatsz. Használd ugyanazt a kapcsolatot, és add meg a tranzakciót: new SQLiteCommand(sql, kapcsolat, tranzakcio).
5. Kétértelmű oszlopnév
SELECT nev, nev FROM diakok d JOIN osztalyok o ON d.osztalyid = o.id;
-- Error: ambiguous column name: nevHiba és javítás
Mindkét táblának van nev oszlopa, ezért az SQL nem tudja, melyiket kéred. Használj táblaneveket vagy rövid neveket: d.nev AS diak, o.nev AS osztaly. Az AS egyedi oszlopnevet is ad az eredménynek.
6. Túl sok sor a JOIN-tól
SELECT d.nev, o.nev FROM diakok d, osztalyok o; -- 21 sor, nem 7!Hiba és javítás
A vesszővel elválasztott táblák között nincs összekapcsoló feltétel, ezért minden sor minden sorral párosul (keresztszorzat). Használj JOIN ... ON feltételt, vagy WHERE d.osztalyid = o.id szűrést.
Mini projekt: osztálynévsor
Állítsd össze a teljes osztálynévsor-programot az iskola.db adatbázissal:
- Az adatbázis az 1. szakasz szerint (két tábla, hét diák); a kapcsolati szöveg a
Foreign Keys=Truebeállítással, az adatbázis a program mellé másolva. - A
DBosztályOsztalyok,DiakokésDiakFelvetelmetódusa; az ablak a szűrővel, a táblázattal és a felvételi résszel. - Új osztály diákokkal: egy többsoros
txtDiakokmezőbe soronként a diákok nevét írja a felhasználó, egytxtOsztalyNevmezőbe az osztály nevét. AbtnOsztalyLetrehozgomb a tranzakciósOsztalyDiakokkalmetódust hívja (az üres sorokat kihagyva), majd frissíti a listákat. Már létező osztálynévnél érthető üzenet jelenjen meg. - Osztály törlése: egy
btnOsztalyTorolgomb törölje a szűrőben kiválasztott osztályt, de csak ha nincs diákja. Ha van, mutasson érthető üzenetet (aFOREIGN KEYhibát kezelve). - Teszteld: nem létező osztályazonosító (
Foreign Keyskikapcsolva és bekapcsolva), azonos nevű osztály, üres névsor, törlés diákokkal és diák nélkül.
Megoldás megnyitása (próbáld ki előbb önállóan!)
A tranzakciós metódus a leckében szerepel. Az ablak kódja a névsort bontja sorokra, az üreseket kihagyva, és a kivételeket külön kezeli: az SQLiteException üzenetében szereplő UNIQUE és FOREIGN KEY szövegek alapján érthető üzenetet ad.
// DB.cs: osztály törlése (diák nélkül)
public static void OsztalyTorles(int id)
{
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteCommand parancs = new SQLiteCommand("DELETE FROM osztalyok WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@id", id);
parancs.ExecuteNonQuery();
}
}
}
// Form1.cs
private void btnOsztalyLetrehoz_Click(object sender, EventArgs e)
{
string osztalyNev = txtOsztalyNev.Text.Trim();
if (osztalyNev.Length == 0)
{
lblUzenet.Text = "Add meg az osztály nevét!";
return;
}
// az üres sorok kihagyása
int db = 0;
foreach (string sor in txtDiakok.Lines)
{
if (sor.Trim().Length > 0)
{
db = db + 1;
}
}
string[] nevek = new string[db];
int i = 0;
foreach (string sor in txtDiakok.Lines)
{
if (sor.Trim().Length > 0)
{
nevek[i] = sor.Trim();
i = i + 1;
}
}
try
{
DB.OsztalyDiakokkal(osztalyNev, nevek);
lblUzenet.Text = $"Az osztály létrejött {db} diákkal.";
// a két ComboBox és a lista újratöltése a Form1_Load logikájával
}
catch (SQLiteException ex)
{
if (ex.Message.Contains("UNIQUE"))
{
lblUzenet.Text = "Ilyen nevű osztály már létezik.";
}
else
{
DB.Naploz(ex);
lblUzenet.Text = "A létrehozás nem sikerült.";
}
}
}
private void btnOsztalyTorol_Click(object sender, EventArgs e)
{
int id = Convert.ToInt32(cmbSzuro.SelectedValue);
if (id == 0)
{
lblUzenet.Text = "Válassz ki egy osztályt a szűrőben!";
return;
}
try
{
DB.OsztalyTorles(id);
lblUzenet.Text = "Az osztály törölve.";
}
catch (SQLiteException ex)
{
if (ex.Message.Contains("FOREIGN KEY"))
{
lblUzenet.Text = "Az osztályt nem lehet törölni, amíg diákjai vannak.";
}
else
{
DB.Naploz(ex);
lblUzenet.Text = "A törlés nem sikerült.";
}
}
}Mit tegyél ide: a kész program a létrehozás részével (osztály neve, diákok névsora egy többsoros mezőben) és a törlés gombbal.Javasolt fájlnév: winforms-3-10-kesz.webp · Alt szöveg: Az osztálynévsor-program osztálylétrehozással
Három önálló gyakorlófeladat
Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.
1. Létszám osztályonként
Egy gomb írja az A lstLetszam ListBoxba osztályonként a diákok számát (10.A: 2), az üres osztályokat is mutatva (11.B: 0).Egy lehetséges megoldás
LEFT JOIN megtartja az üres osztályokat, a COUNT(d.id) pedig csak a létező diákokat számolja (nem COUNT(*)). Az olvasót (ExecuteReader) a 3.9 leckéből ismered.lstLetszam.Items.Clear();
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();
string sql = "SELECT o.nev AS osztaly, COUNT(d.id) AS letszam FROM osztalyok o LEFT JOIN diakok d ON d.osztalyid = o.id GROUP BY o.id, o.nev ORDER BY o.nev";
using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
using (SQLiteDataReader olvaso = parancs.ExecuteReader())
{
while (olvaso.Read())
{
lstLetszam.Items.Add(olvaso["osztaly"] + ": " + olvaso["letszam"]);
}
}
}
2. Diákok áthelyezése és az üres osztály törlése
A Két művelet, amely együtt egy egységet alkot: tranzakció. Ha az osztály törlése valamiért nem sikerül, az áthelyezés is visszavonódik. Mindkét parancs ugyanazt a kapcsolatot és tranzakciót használja.btnAthelyez gomb helyezze át az egyik osztály összes diákját egy másik osztályba, majd törölje a most már üres osztályt. A két lépés együtt sikerüljön vagy együtt bukjon.Egy lehetséges megoldás
public static void OsztalyAthelyezes(int forrasId, int celId)
{
using (SQLiteConnection kapcsolat = UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteTransaction tranzakcio = kapcsolat.BeginTransaction())
{
using (SQLiteCommand athelyez = new SQLiteCommand("UPDATE diakok SET osztalyid = @cel WHERE osztalyid = @forras", kapcsolat, tranzakcio))
{
athelyez.Parameters.AddWithValue("@cel", celId);
athelyez.Parameters.AddWithValue("@forras", forrasId);
athelyez.ExecuteNonQuery();
}
using (SQLiteCommand torol = new SQLiteCommand("DELETE FROM osztalyok WHERE id = @forras", kapcsolat, tranzakcio))
{
torol.Parameters.AddWithValue("@forras", forrasId);
torol.ExecuteNonQuery();
}
tranzakcio.Commit();
}
}
}
3. ON DELETE SET NULL
Hozz létre az A törlés sikerül, a 10.B diákjainak iskola2.db adatbázisban ugyanilyen két táblát, de az idegen kulcs legyen ON DELETE SET NULL. Töröld a 10.B osztályt, és nézd meg, mi történik a diákjaival. Mikor jobb ez, mint a RESTRICT viselkedés?Egy lehetséges megoldás
osztalyid értéke NULL lesz (a diákok megmaradnak, csak osztály nélkül). Hasznos, ha a hivatkozott sor megszűnése nem jelenti a hivatkozók megszűnését (például egy osztály megszűnik, de a diákok az iskolában maradnak). Ahol a hivatkozott sor nélkül a hivatkozó értelmetlen (például egy megrendelés sorai megrendelés nélkül), a CASCADE vagy a tiltás (RESTRICT) a helyes.CREATE TABLE diakok (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nev TEXT NOT NULL,
osztalyid INTEGER,
FOREIGN KEY (osztalyid) REFERENCES osztalyok (id) ON DELETE SET NULL
);
DELETE FROM osztalyok WHERE id = 2;
SELECT * FROM diakok; -- a korábbi 10.B diákok osztalyid oszlopa NULL
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 az iskola.db adatbázissal, az utolsó kettő C# feladat. Csak a próbálkozás után nyisd le a megoldást.
Alap szint
1. Új tábla kapcsolattal
Hozz létre egy Egy tábla több idegen kulcsot is tartalmazhat. A tantargyak táblát (id, nev), és egy jegyek táblát (id, diakid, tantargyid, jegy 1–5 közötti számmal). A jegyek két idegen kulcsot tartalmazzon.Megoldás megnyitása
CHECK kikötés az érték tartományát korlátozza: az adatbázis 1–5 közötti jegyet enged csak.CREATE TABLE tantargyak (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nev TEXT NOT NULL UNIQUE
);
CREATE TABLE jegyek (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diakid INTEGER NOT NULL,
tantargyid INTEGER NOT NULL,
jegy INTEGER NOT NULL CHECK (jegy BETWEEN 1 AND 5),
FOREIGN KEY (diakid) REFERENCES diakok (id),
FOREIGN KEY (tantargyid) REFERENCES tantargyak (id)
);
2. Diákok az osztályukkal
Írj lekérdezést, amely a diákok nevét és az osztályuk nevét mutatja osztály, azon belül név szerint rendezve (csak a osztállyal rendelkezők). Az Megoldás megnyitása
INNER JOIN a osztály nélküli diákot kihagyja. A d és o rövid név teszi egyértelművé az oszlopokat.SELECT d.nev AS diak, o.nev AS osztaly
FROM diakok d
INNER JOIN osztalyok o ON d.osztalyid = o.id
ORDER BY o.nev, d.nev;
Közepes szint
3. Kik nincsenek osztályban?
Add meg a diákok nevét, akiknek nincs osztályuk, két módon: egy A kérdés megoldható egyetlen táblából is, mert az LEFT JOIN és egy egyszerű WHERE segítségével.Megoldás megnyitása
osztalyid maga NULL. A LEFT JOIN változat ugyanazt adja, de a JOIN ilyenkor felesleges. Hasznos akkor lenne, ha az osztályok adataira is szükség van.SELECT nev FROM diakok WHERE osztalyid IS NULL;
SELECT d.nev
FROM diakok d
LEFT JOIN osztalyok o ON d.osztalyid = o.id
WHERE o.id IS NULL;
4. A legnagyobb osztály
Add meg a legtöbb diákot tartalmazó osztály nevét és létszámát. Osztályonként összeszámoljuk a diákokat (Megoldás megnyitása
GROUP BY), csökkenő létszám szerint rendezzük, és az első sort kérjük (LIMIT 1). Egyenlőség esetén csak az egyik osztályt kapod meg.SELECT o.nev AS osztaly, COUNT(d.id) AS letszam
FROM osztalyok o
LEFT JOIN diakok d ON d.osztalyid = o.id
GROUP BY o.id, o.nev
ORDER BY letszam DESC
LIMIT 1;
Kihívás szint
5. A ComboBox kezdőértéke és a kiválasztás
Írd meg, hogy a A cmbOsztaly egy diák szerkesztésekor a diák saját osztályát mutassa kiválasztva, osztály nélküli diáknál pedig az első, (nincs) elemet. Hogyan állítod be programból a kiválasztott értéket?Megoldás megnyitása
SelectedValue tulajdonság értéke beállítható: az adott azonosítójú elem lesz kiválasztva. Ha az azonosító nem szerepel a listában, nincs kiválasztott elem (SelectedIndex: -1), ezért érdemes kezelni. Az osztály nélküli diák azonosítója az (összes) elemmel megegyező 0 kell legyen, hogy a lista első eleme legyen kiválasztva.int osztalyId = 0; // 0: nincs osztálya
if (!sor.IsNull("osztalyid"))
{
osztalyId = Convert.ToInt32(sor["osztalyid"]);
}
cmbOsztaly.SelectedValue = osztalyId;
if (cmbOsztaly.SelectedIndex < 0)
{
cmbOsztaly.SelectedIndex = 0; // ismeretlen azonosító: az első elem
}
6. A tranzakció visszavonásának próbája
Írj egy gombot, amely egy tranzakcióban két diákot vesz fel, de a második után szándékosan kivételt dob ( A throw new Exception("próba")). Ellenőrizd a DB Browserben, hogy egyik diák sem került be. Mi történik, ha a kivétel dobása elé teszed a Commit hívást?Megoldás megnyitása
Commit előtti kivétel esetén a tranzakció visszavonódik, egyik diák sem marad bent. Ha a Commit a kivétel előtt fut le, az összes diák véglegesen megmarad, mert a véglegesítés megtörtént. Ez mutatja, hogy a Commit helye dönti el, mit véd a tranzakció.private void btnProba_Click(object sender, EventArgs e)
{
try
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();
using (SQLiteTransaction tranzakcio = kapcsolat.BeginTransaction())
{
using (SQLiteCommand parancs = new SQLiteCommand("INSERT INTO diakok (nev) VALUES (@nev)", kapcsolat, tranzakcio))
{
parancs.Parameters.AddWithValue("@nev", "Próba Egy");
parancs.ExecuteNonQuery();
parancs.Parameters["@nev"].Value = "Próba Kettő";
parancs.ExecuteNonQuery();
}
throw new Exception("próba"); // a Commit előtt
tranzakcio.Commit();
}
}
}
catch (Exception ex)
{
lblUzenet.Text = "A művelet megszakadt: " + ex.Message + " (a két diák nem került be)";
}
}
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. Egy oszlop, amely egy másik tábla elsődleges kulcsára hivatkozik, és így összekapcsolja a két tábla sorait. Kapcsolatonként be kell kapcsolni: a C# kapcsolati szövegben Az A Több művelet oszthatatlan egysége: vagy mind sikerül ( A blokk elhagyásakor a tranzakció visszavonódik, és az összes módosítás elvész, hibaüzenet nélkül. A másik kapcsolat nem része a tranzakciónak, és az SQLite database is locked hibát adhat. A tranzakcióban egy kapcsolatot használj.1. Mi az idegen kulcs?
2. Mit kell tenni, hogy az SQLite ellenőrizze az idegen kulcsot?
Foreign Keys=True;.3. Mi a különbség az INNER JOIN és a LEFT JOIN között?
INNER JOIN csak a mindkét oldalon párt találó sorokat adja. A LEFT JOIN a bal oldali tábla minden sorát megtartja, a hiányzó párt NULL értékkel tölti ki.4. Mire való a DisplayMember és a ValueMember?
DisplayMember az oszlop, amelyet a felhasználó lát, a ValueMember az, amelyet a program kap a SelectedValue tulajdonságból (például az azonosító).5. Mi a tranzakció, és mikor kell?
Commit), vagy egyik sem (Rollback). Akkor kell, ha több összefüggő módosítás együtt értelmes.6. Mi történik, ha a using blokkban elmarad a Commit?
7. Mi a baj, ha a tranzakció alatt másik kapcsolatot nyitsz az írásra?
Mit tudsz már?
- Két táblát kapcsolsz össze elsődleges és idegen kulccsal, és bekapcsolod az ellenőrzést.
- JOIN lekérdezéssel olvashatóvá teszed a kapcsolt adatot.
- A ComboBox
DisplayMember/ValueMemberpárosával nevet mutatsz, azonosítót kezelsz. - Tranzakcióba foglalod az összetartozó műveleteket.
- Érted a kulcssértés és a visszavonás hibaüzeneteit.
Következő lecke: záró projekt – nyilvántartás
A 3. fejezet eszközei mind megvannak. A záró projektben egy teljes nyilvántartást építesz fel: több tábla, kereső, szerkesztés, védelem és hibakezelés.




