Od koda do podataka — zatvoren krug

Kako klasa sa kolonama postane SQL: drajver, RDC, JOIN pri addColumn, dva upita po ekranu

↑ Nazad

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č.


1. Ko odlučuje da li ide u SQL ili u memoriju

Odluku ne donosi nasleđivanje, nego parametar. Konstruktor GDbTable(String, Object...) radi tri koraka:

  1. Prođe kroz varargs i traži vrednost tipa GDataServer.Type (GDbTable.java:76-83).
  2. Ako ga nema, uzme tip iz aktivne baze: GDatabase.getActiveDatabase().getDataServer().getType().
  3. switch po tipu (:104-112), a enum ima samo dve vrednosti:
  4. MYSQL → new GDataDriverMySql(baza, tabela, url)
  5. MEMORY → new GDataDriverMemory()

Zato važi jednostavno pravilo za čitanje koda:

klasagde živi
extends DbtObjectu bazi (tip iz aktivne baze)
extends MemDbtObjectu memoriji — konstruktor sam ubaci Type.MEMORY u varargs
extends DbtObject + ručno prosleđen Type.MEMORYu 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.

2. Šta se nasleđuje — kičma u lancu konstruktora

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.


3. JOIN se definiše pri deklaraciji kolone, ne pri upitu

Ovo 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:

  1. samu kolonu stranog ključa,
  2. proširenu kolonu koja nosi opis iz ciljne tabele (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.

Šta je RDC

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:

Posledica: 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 = 0

JOIN ide na primarni ključ ciljne tabele, a prikazuje se njen RDC.

Zašto 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.


4. Kako upit ode i kako se podaci vrate

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:


5. Jedan ekran = dva upita, iz dva različita koda

Ovo se lako pogrešno pročita u tragu.

upitodakle
gridcela tabela, uz permanentni filterGDbTable.loadData() :169 → GDataDriverMySql.loadData :70
tekući slogjedan red po primarnom ključudirektno GMySqlDataLink.executeQuery :119, bez loadData

Dve posledice:

Ekran sa stranim ključem plaća celu ciljnu tabelu

Redosled upita pri otvaranju ekrana koji ima jedan strani ključ:

  1. SELECT nad celom ciljnom tabelom (openForeignKeyTables())
  2. podešavanja kolona za ciljnu tabelu, dva upita
  3. tekući slog ciljne tabele, dva upita
  4. tek sada grid traženog ekrana
  5. podešavanja kolona za traženi ekran, dva upita
  6. tekući slog traženog ekrana, dva upita

Za malu ciljnu tabelu to je besplatno. Za strani ključ ka velikoj tabeli to je zid skaliranja.


6. Ponašanja koja se vide samo u izvršavanju

Izmereno, ponovljeno na dva različita ekrana:

  1. SHOW KEYS posle svakog SELECT-a — metapodaci se ne keširaju; svaki upit plaća još jedan obilazak baze.
  2. Upit za izabran red ide dvaput, identično. Sa pratećim SHOW KEYS to su četiri obilaska baze za jedan red. Ponašanje engine-a, ne tabele — potvrđeno na dva ekrana.
  3. openForeignKeyTables() se zove i kad tabela nema ni jedan strani ključ.
  4. Podešavanja kolona se čitaju u dva nivoa — prvo za korisnika, pa za podrazumevano.
  5. Amplifikacija ne zavisi od veličine tabele. Na tabeli od dve kolone, 212 od ~284 poziva (75 %) bilo je puko ispitivanje stanja; stvarnog posla četiri poziva.
  6. Metadata Mapping se zatvara u krug — objekti kolona koje addColumn napravi pri konstrukciji ekrana su isti objekti koji se kasnije predaju pretvaranju vrednosti za svaku ćeliju.

Kako čitati trag — pet pravila naučenih kroz greške

  1. 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.
  2. Linija je u klasi gde je metoda deklarisana, ne odakle je pozvana. Trag odgovara na „koja metoda se izvršila", ne „ko ju je zvao". Pozivalac se vidi uz --stack.
  3. 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.
  4. Broj poziva je svojstvo filtera, ne programa. Isti klik: 359 poziva pod jednim profilom, 794 pod širim. Poredi samo unutar istog profila.
  5. Vrednosti argumenata se čitaju bez poziva metoda — po dizajnu. Zato objekat ostaje neproziran (ActionEvent@3299), a konstruktorski argumenti se vide odlično i tu je najveća vrednost.

Izvor: 11-od-koda-do-podataka.md