Kako klasa sa kolonama postane SQL: drajver, RDC, JOIN pri addColumn, dva upita po ekranu
Ova stranica zatvara pitanje koje je dugo stajalo otvoreno: kad u Javi napišem klasu sa nekoliko kolona, šta se tačno desi da bi se u prozoru pojavili podaci iz baze? Sve niže je izmereno na pravoj aplikaciji tracerom, pa provereno u izvoru — ne pretpostavljeno.
Radni primer je najprostiji ekran u projektu: klasa od 28 linija, dve kolone, nijedan strani ključ.
Odluku ne donosi nasleđivanje, nego parametar. Konstruktor GDbTable(String, Object...) radi tri koraka:
GDataServer.Type (GDbTable.java:76-83).GDatabase.getActiveDatabase().getDataServer().getType().switch po tipu (:104-112), a enum ima samo dve vrednosti:MYSQL → new GDataDriverMySql(baza, tabela, url)MEMORY → new GDataDriverMemory()Zato važi jednostavno pravilo za čitanje koda:
| klasa | gde živi |
|---|---|
extends DbtObject | u bazi (tip iz aktivne baze) |
extends MemDbtObject | u memoriji — konstruktor sam ubaci Type.MEMORY u varargs |
extends DbtObject + ručno prosleđen Type.MEMORY | u memoriji |
MemDbtObject nije zaseban mehanizam — to je DbtObject koji je unaprijed popunio taj jedan parametar.
Zamka: ako dobavljanje aktivne baze baci izuzetak, tip pada na MEMORY. Tabela tiho postane memorijska, bez greške. Vidi stranicu o praznim ekranima.
Trag otvaranja ekrana pokazuje lanac od najizvedenije klase ka osnovi:
Firma.<init>()
GDbTable.<init>("Firma", Object[0])
GDataSource.<init>("Firma")
GDataSourceLevel20.<init>("Firma")Numerisana kičma Level00 → 05 → 10 → 20 → 30 → 40 → 50 → GDataSource nije dekoracija — svaki nivo dodaje jedan sloj odgovornosti. Level20 drži redove i kolone, Level30 RDC listu, Level40/50 poziv bez GUI-ja.
JOIN se definiše pri deklaraciji kolone, ne pri upituOvo je bilo najveće otkriće. Kad se doda kolona koja pokazuje na drugu tabelu, GDataSourceLevel20.addColumn (:58-82) ne doda jednu kolonu nego dve:
createExpandedColumn), dodatu rekurzivnim pozivom addColumn,i uz to registruje addJoin(new GJoin(..., imeCiljneTabele, imeCiljneKolone)).
Zamka u imenovanju: taj mehanizam se u kodu zove Decryptable, a nema nikakve veze sa šifrovanjem. Znači: „ovu kolonu stranog ključa proširi dodatnom kolonom koja prikazuje opis ciljne tabele". Zato deklaracije nose DECRYPT_VISIBLE_NO.
RDCList je „niz kolona i stringova koji opisuju sadržaj tekućeg sloga" (GDataSourceLevel30.java:23) — kako se jedan red predstavlja čoveku. Ako se ne postavi, engine uzme primarni ključ, što je za mašinu tačno a za čoveka beskorisno. Zato tabela klijenata kaže setRDCItem(getColumn("Opis")): „kad neko pokaže na moj red, prikaži opis, ne šifru".
Dva mesta gde RDC postaje SQL:
GColumnForeign.java:37 → kolona stranog ključa nasleđuje RDC listu ciljne tabeleGColumnHeader.java:1336 → ta lista sama generiše svoj deo SELECT-a, sa prefiksom tabelePosledica: N addColumn ne daje N stavki u SELECT-u. Mereno na ekranu sa 6 kolona od kojih je jedna strani ključ — SELECT ima 7 stavki:
SELECT t0.Id, t0.Firma, concat(trim(t1.Opis)),
t0.Opis, t0.Verzija, t0.InputStyle, t0.Arhiviran
FROM Definition AS t0
LEFT OUTER JOIN Firma AS t1 ON t0.Firma = t1.Sifra
WHERE t0.Arhiviran = 0JOIN ide na primarni ključ ciljne tabele, a prikazuje se njen RDC.
concat(trim(...))Zato što je kolona char(120), a char popunjava razmacima. Ista činjenica iz tri ugla: char(120) u DDL-u → 'AL ' u WHERE-u → trim() u SELECT-u. Stavka za budući port: u PostgreSQL-u se char(n) popuna ne ponaša isto u poređenjima.
SASTAVLJANJE GDataDriverMySql.getSelectString :99
GDataDriverMySql.getFilters :139 -> WHERE
GDataDriverMySql.getOrder :187 -> ORDER BY
IZVRSAVANJE GMySqlDataLink.executeQuery :117
GLog.println(DATABASEMYSQL, "*" + sql + "*")
mStatement.executeQuery(sql)
VRACANJE GDataDriverMySql.importData :259
while(mDataSet.next()) {
createRowDataSource() // objekat po REDU
for i: set(i, dbmsValueToEngineValue(
mDataSet.getObject(i+1), getColumn(i))) // po POZICIJI
setKeyValue(...); addRow(...)
}Tri stvari koje iz ovoga slede:
getSelectString, getFilters, getOrder, dbmsValueToEngineValue, plus GMySqlDataLink za vezu.Statement, ne PreparedStatement. Vrednosti se ugrađuju u tekst upita — odatle i vidljiva char popuna u tragovima. Nema keširanja plana na serveru, i postoji površina za SQL injection kroz vrednosti polja.getObject(i+1)), ne po imenu. Zato je redosled addColumn nosiv — promena redosleda kolona u klasi menja mapiranje. A pozicije se poklapaju upravo zato što je proširena RDC kolona već u listi kolona, na istom mestu gde je i u SELECT-u.Ovo se lako pogrešno pročita u tragu.
| upit | odakle | |
|---|---|---|
| grid | cela tabela, uz permanentni filter | GDbTable.loadData() :169 → GDataDriverMySql.loadData :70 |
| tekući slog | jedan red po primarnom ključu | direktno GMySqlDataLink.executeQuery :119, bez loadData |
Dve posledice:
WHERE Arhiviran = 0 nije „nema filtera" — to je permanentni filter koji je izašao kroz getFilters(). Ko upit prepiše u klijent bez tog uslova, ne gleda isto što aplikacija.WHERE Id = <n> nije grid — to je osvežavanje tekućeg sloga, i ne nosi permanentni filter. Ako se u tragu vidi samo on, deluje kao da program čita jedan red; grid je stotinama linija ranije.Redosled upita pri otvaranju ekrana koji ima jedan strani ključ:
SELECT nad celom ciljnom tabelom (openForeignKeyTables())Za malu ciljnu tabelu to je besplatno. Za strani ključ ka velikoj tabeli to je zid skaliranja.
Izmereno, ponovljeno na dva različita ekrana:
SHOW KEYS posle svakog SELECT-a — metapodaci se ne keširaju; svaki upit plaća još jedan obilazak baze.SHOW KEYS to su četiri obilaska baze za jedan red. Ponašanje engine-a, ne tabele — potvrđeno na dva ekrana.openForeignKeyTables() se zove i kad tabela nema ni jedan strani ključ.addColumn napravi pri konstrukciji ekrana su isti objekti koji se kasnije predaju pretvaranju vrednosti za svaku ćeliju.fajl:linija je prva izvršna linija tela metode, ne linija deklaracije. Potpis se traži 1–3 linije iznad broja iz traga. Pomeraj je sistematski — nije znak da su JAR i izvor iz različitih vremena.--stack.watch= bez tačke na kraju je tačna klasa — i tiho sakriva nadklase. Kad metoda „mora da se zove a nema je u tragu", prvo proveri u kojoj je klasi deklarisana, pa je dodaj u watch=; tek onda sumnjaj na program.ActionEvent@3299), a konstruktorski argumenti se vide odlično i tu je najveća vrednost.