SQL som applikations språk

Mjukvara är skriven i programmeringsspråk. Dessa språk delas upp på olika sätt som hög- och lågnivå eller i generationer som ”fjärde generationens språk”. SQL är ett sådant ”fjärde generationens” språk. Tanken var att kunna beskriva vad som önskas från datorn istället för på ett exakt vis koda hur informationen skall samlas ihop på. Gärna att personer utan programmeringsbakgrund skulle kunna deklarera egna frågor då språken ofta försöker efterlikna vanlig engelska.

stateDiagram-v2
        State1: 1 Gen
        State1 --> State2
        State2: 2 Gen
        note left of State2 : Assembly
        State2 --> State3
        State3: 3 Gen
        note left of State3 : C, Java, Python
        State3 --> State4
        State4: 4 Gen
        note left of State4 : SQL, ABAP
        State4 --> State5
        State5: 5 Gen
        note left of State5 : AI? LLM?

Tanken är bra och det är också vad dagens low-code lösningar försöker göra igen. Alltså höja abstraktionsnivån med hjälp av färdigskriven kod som förhoppningsvis blir på en högre och enklare nivå. Detta nya verktyg kan vara allt från lite färdiga funktioner i ett befintligt språk eller en hel plattform där du drar och släpper grafiska objekt på varandra som ett sätt att programmera maskinen.

Men ett lager till av abstraktion har ett pris. Det kan vara omöjligt att beskriva en lösning på vissa problem med den detaljrikedom som funnits till hand med ett ”vanligt” programmeringsspråk på en lägre nivå. Alternativt blir den SQL-baserade lösningen onödigt komplex för att det inte är möjligt att återanvända kod på samma sätt.

Detta leder mig till kärnan av detta inlägg: Stored procedures som språk i backend. Alltså system där det mesta av affärslogiken (det som är värt pengar) är inlåst i SQL, ett språk där inga satsningar görs på ergonomin för utvecklaren. Ett språk som saknar arv, klasser, inkapsling, reflektion, generics och annat gött som övriga språk uppfinner just för att hantera en skenande komplexitet.

Let me put this another way: Stack Overflow has only 1 stored procedure in the database and I intend to move that last vestige into code.

https://nickcraver.com/blog/2016/02/17/stack-overflow-the-architecture-2016-edition/

Ovan är citat från arkitekten på Stackoverflow och vad denna känner stored procedures har för plats i systemet.


Hur kom vi hit?

På 90-talet när relationsdatabaser fick ordentligt genomslag så gissar jag att det sågs som en smidig lösning att uppnå en klient-server arkitektur genom att skapa en ”fet klient” i till exempel VB eller C++ som blev hårt knyten till sin server som också samtidigt är en databas där ”koden” är i form av stora mängder SQL procedurer.

Målet var att avlasta de kanske svagare klientdatorerna med att de (misstänkt) allra tyngsta funktionerna kördes direkt i databasen, på en annan maskin. Samt att löftet var också att fjärde generationens språk skulle vara mer lämpade och kraftfullare i att beskriva affärsprocesserna i mjukvaran.

graph LR; 
Klient1-->|SQL|D[(Databas)] 
Klient2-->|SQL|D 
Klient3-->|SQL|D 
E(ev. Servertjänst)-->|SQL|D 

Klienterna hämtade data och utförde förändringar i databasen genom att skicka SQL strängar synkront ”över tråden”.

Resultatet blev att system byggdes upp med två mycket olika programmeringsspråk. Där det ena förmodligen var ett tredje generations objekt-orienterat språk i klienten, och det andra ett fjärde generations data-fråge språk.

pie title Andel programmeringsspråk i ett hypotetiskt system
    "VB" : 15
    "SQL" : 85

Databasen kan inte i denna arkitektur vara den första att reagera och initiera kontakt tillbaka till en klient när någon ny händelse inträffat. Klienterna får istället konstant polla dabasen efter intressanta ändringar att reagera på. Önskar användarna att systemet skall bli rappare är enda alternativet att klienterna pollar hårdare. Detta är bara en form av busy waiting, dåligt för miljön.

Att bygga ett system med två så pass olika språk är en onödig kostnad i den mentala energi det går åt att byta mellan dessa. Förutom att språken som används skiftar konstant så skiftar också verktygen som används, sättet ändringar testas, sättet ändringar versionshanteras på och sättet ändringar transporteras till produktion.

Föreläsaren pratar här om att historien upprepar sig med dagens webb ”applikationer” som körs lokalt på användarens dator.

graph LR; 
Klient1-->|HTTP|B((Backend)) 
Klient2-->|HTTP|B 
Klient3-->|HTTP|B 
B-->|SQL|D[(Databas)] 

I dessa system återkommer det här problemet i form av att det webbaserade användargränssnittet görs interaktivt med Javascript medans något annat språk sannolikt används för att implementera bakomliggande API. Kanske finns även här också logik skrivet med SQL i databasen vilket gör att 3 olika språk används i systemet.

pie title Språk i "modern" "webapp"
    "TypeScript" : 45
    "Python" : 45
    "SQL" : 10

Det är inte konstigt att behovet av ”frontend” utvecklare skapades när det blev normalt att använda flera programmeringsspråk och verktyg inom samma system. Innan dess, enligt Simon Martinelli i klippet ovan, var vi alla ”fullstack” och detta var det normala. Det fanns inget annat ord än bara ”utvecklare”.

Men detta inlägg skall inte handla om arkitektur, det skall handla om hur det är att utveckla i SQL med allt vad det innebär för både funktionella och icke-funktionella behov.


Programmeringen

Testdriven utveckling är inget som förekommer i den här typen av programmering. Stega sig igenom (debugga) koden är heller inget som går att göra, mer om det nedan under felsökning.

I objektorienterade språk finns möjligheter i språket som kan användas till att undvika att skjuta både sig själv i foten och andra som skall underhålla koden i i framtiden. Genom superklasser och konstruktorer kan du till exempel garantera att ditt objekt (och för dig okända framtida arvsklasser) skapas upp i rätt tillstånd. Avancerade ramverk som Spring använder sig av Aspect-oriented programming och reflektion för att bjuda in till att förenkla och modularisera systemet ytterligare. Inget av detta existerar i SQL ekomiljön.

I SQL procedurer är det i stället konventioner som måste följas. Till exempel saker som måste placeras längst upp i alla procedurer. Eller komplex felhanteringslogik som måste kopieras och klistras in i varje procedur. Detta kunde lätt byggas dynamiskt, datadrivet och underhållas i en enda tappning på en central plats i ett konventionellt språk.

  • Editorn du sitter i är förmodligen inte lika kraftfullt som ett vanligt IDE så som Visual Studio eller Eclipse och förmodligen gjord för att användas både av utvecklare, de som tar backup och någon som vill skapa en rapport
  • Inte säkert att syntax highlighting eller mörkt tema finns.
  • Troligen saknas integration till versionshanteringssystemet.
  • Om du trycker på ett procedur anrop öppnas inte implementationen.
  • Inget stöd för automatisk refaktorisering.

In contrast, stored procedures require the skilled DBA to write SQL, which is far from as straightforward as application code. A stored-procedure-heavy architecture is one that is either terrible because of bad stored procedures, or requires substantial talent of the DBA writing the stored procedures to do them properly.

https://medium.com/@JoeEmison/stored-procedures-as-a-litmus-test-c7ec772b985e
  • Skall du köra din kod behöver du koppla upp dig mot en databasserver och säkerställa att koden i dit fönster är den aktuella koden som också lagrats på servern.
  • Det är generellt en uppförsbacke att bryta ut och skapa återanvändbar kod då dessa också blir likvärdiga procedurer som allt annat i det globala scopet utan något sätt att kapsla in (gömma) de i klasser, paket, moduler etc.

Det är konstigt att fjärde generationens språk som var förhoppningen till framtida bättre mjukvara har blivit omkörd av den tidigare generationens moderna verktyg och designmönster, fast dessa språk är äldre.


Felhantering

En bra taktik i all systemutveckling är ”fail-fast”. Du vill alltså att om minsta lilla grej går fel i en av dina allra största operationer som hämtar och ändrar data från stora delar av din databas att koden direkt skall sluta köras, kasta fel som loggar vilken rad i din kod där undantaget uppstod och återställ all data som hittills ändrats till utgångsläget.

Du tänker att detta är varför vi har en databas och att en transaktion runt alla procedurer som skall köras löser detta? Njaa vi tar MS SQL Server som exempel på hur mycket detaljer man faktiskt behöver känna till för att innehållet i databasen inte skall bli ”korrupt” (helt fel att säga korrupt).

Läs lite grann i den här serien om två tabeller som skall ändras: Error Handling Quiz Week: Tryin’ TRY/CATCH

För att ha en chans att få ett stabilt system skrivet med T-SQL där koden slutar köras vid fel behöver du alltså ha try-catch runt all din kod, hela tiden, i varje procedur. Men:

So to recap, TRY/CATCH doesn’t catch small problems or big problems – only medium-sized problems.

Ok, så vi lägger till transaktioner. Men:

I know, right? You thought that by combining try/catch with a transaction, you’d get robust error handling.  Instead, you ended up with half the tables populated, and a leftover open transaction. You’d already forgotten Tuesday’s post in which I pointed out that TRY/CATCH doesn’t catch low severity or high severity errors.

Du behöver tydligen komma ihåg att lägga till någon grej som heter XACT_ABORT överallt hela tiden:

We start with the XACT_ABORT option which increases our odds that entire transactions are rolled back or forwards together. However, that alone still may not be enough – remember Tuesday’s post where I talked about how some catastrophic errors skip the CATCH block altogether

For good error handling in SQL Server, you need both TRY-CATCH and SET XACT_ABORT ON. Of these two, SET XACT_ABORT ON is the most important. For production-grade code it’s not really sufficient to rely on XACT_ABORT, but for quick and simple stuff it can do.

Error and Transaction Handling in SQL Server

Det ÖKAR (!?) chansen att det kommer fungera?


Felsökning

Det finns ett ansvar hos utvecklare att skapa icke-funktionella förutsättningar för att kunna observera systemet under drift samt felsöka det djupare i testbänk.

Den största klassikern för felsökning är att sätta en breakpoint och stega genom koden samtidigt som du undersöker värdet på variablerna i programmet. Hur gör vi detta i SQL server? Det gör vi inte. Det fanns tidigare i SSMS men har nu tagits bort. Brent Ozar föreslår istället att köra lite olika varianter av att ”skriva till konsolen”.

Det som oftast hjälper mig att lösa problem i ett system är om hela stackens spårning sparas ut till systemets logg. Här ses uppifrån och ned vad för typ av fel det var som avbröt anropskedjan och därefter listas varje klass och vilken rad i koden där följande funktionsanrop gjordes för att komma dit där det smällde. Det är fantastiskt att kunna få automatiskt vilken rad i koden problemet befinner sig på! Raden blir mer exakt ju hårdare fail-fast görs.

I Eclipse trycker jag enkelt på ”Kantela.java:111” som tar mig direkt till rad 111 i klassen där undantaget kastades. Jag ser på metodnamnet innan på rad 219 att något gått fel när ett dubbelklick gjorts. Tyvärr känner jag inte till något sätt att få denna superkraft i SQL server där jag tänker mig att jag vill ha en lista med varje procedur som anropades samt vilken rad följande anropades vid.

Prestandan i systemet är svår att mäta när alla användare som sitter på respektive klient när som helst (och i olika konstellationer) skickar mer eller mindre resurskrävande SQL frågor direkt till databasen utan någon möjlighet att aktivt köa, prioritera eller rent av neka (för stunden) oviktiga förfrågningar.


Kontinuerlig utveckling

Det vedertagna sättet att väva in en relationsdatabas i sitt systems kontinuerliga utveckling är med hjälp av migrationer som tar databasens schema till den aktuella versionen. Förslagsvis med verktyg som Flyway.

Dock är dessa verktyg bästa på att hantera delta ändringar av själva databas schemat. Men kan ändringar av din affärslogik i SQL enkelt driftsättas på detta sätt? Det är inte allt för trevligt att skapa upp en procedur på ett antal hundra rader SQL i en migrering, för att sedan göra en mindre ändring på en enstaka rad men ändå bli tvingad att droppa och skapa upp hela proceduren med alla sina rader i en framtida migrering.

because stored procedures are deployed separately from the application, and, because there is usually only one database master, the deployment of stored procedures is usually done separately from application deployments and by separate staff

https://medium.com/@JoeEmison/stored-procedures-as-a-litmus-test-c7ec772b985e

Ett annat vanligt förekommande arbetssätt är att sonika ändra direkt i produktionsmiljön (kanske med sina initialer och datum) och manuellt klippa och klistra in ändringarna (senare) i versionshanteringssystemet. Här sätts migreringar, versionshantering och förmågan att resonera kring systemets samlade komponenter ur spel när det inte längre går att peka på systemet och säga ”jag vet exakt vad som körs här”, det är denna commit.

In contrast, ORMs are just part of regular application code and don’t create any dependencies or requirements on any IT staff beyond whatever existing dev/test/QA/prod processes already exist for the application servers

https://medium.com/@JoeEmison/stored-procedures-as-a-litmus-test-c7ec772b985e

Vad säger litteraturen?

Så här säger en kursbok som används på minst ett svenskt lärosäte:

”Bättre systemstruktur (punkt)” 🤨. Se nästa sektion angående konsistenta uppdateringar och fantomer.

Kontroll av indata är mycket viktigt och om du väljer att göra det i T-SQL får du automatiskt hjälp av korta av alla dina strängar automatiskt (sarkasm) till rätt längd innan de stoppas in i tabellen: SQL Server silently truncates varchar’s in stored procedures.

Om vi nu skall använda SQL som programmeringsspråk är det otroligt att variabler tyst kan förändras när de skickas med som argument till en procedur. I ett vanligt språk inträffar inte denna klass av problem och strängen kunde också omslutas i ett lämpligt domänklass där en instans av detta objekt istället kunde skickas till funktionen och på så sätt få en typsäkerhet vid kompileringen att rätt form av data används i alla anrop runtom i kodbasen.

Till en bra klient förväntas validering göras kontinuerligt med feedback per fält allt eftersom användaren matar in data. Finns ingen längre som vill vänta på resultatet av en procedur i databasen.


Hur skall vi göra istället?

Fundera på vad du vill att databasens uppgift skall vara. Det främsta problemet det här stora beroendet du lyfter in i din arkitektur skall lösa är att bibehålla informationen i systemet vid en omstart. Helst skall bara det nödvändigaste lagras som behövs för att starta upp systemet igen. Annan information kan beräknas fram vid behov i minnet.

Undvik triggers, stored procedures och annan funktionalitet databasmotorn erbjuder som ett sätt att representera funktionalitet. Se SQL som ett assembly språk som kan tas till när den sista extra prestandan måste klämmas ut.

Our goal should be to get out of the SQL crafting business and get back into the data access business.

SQL is the assembly language of the modern world

Fundera på om allt du kanske behöver är SQLite. Går utmärkt att använda som lagring i webbaserade system med flera användare. SQLite har också isolations nivån SERIALIZABLE vilket är det sätt man kan tro alla databaser fungerar på. Annars rekommenderas att läsa på om detta så ingen fantom kommer och tar dig.

Furthermore, even if you could know exactly which concurrency bugs are possible for a particular isolation level, writing an application in a way that these bugs will not happen in practice (or if they do, that they will not cause negative experiences for the users of the application) is also very challenging

Som standard använder alltså de flesta databasmotorer någon form av svag isolation mellan andra samtidiga läsningar och skrivningar. Detta gör att en form av ”race condition” kan göra att tillstånd som inte borde kunna representeras (enligt schema och applikation) ändå i enstaka fall, beroende på vad som händer i övrigt i systemet, lyckas skrivas till disk.

Datan blir ”korrupt” fick jag höra en gång, men systemet gör faktiskt precis det de är designat för under rådande inställningar, och datan ligger där på disk korrekt representerad, dock i sammanhanget i ett tillstånd ingen kunnat förutse.


Fast SQL är bra

Jag har stor respekt för hur kraftfullt SQL är som datarelaterat språk och använder det gärna. Ett genomtänkt schema och lite SQL kunskap kommer man långt på.

Vi behöver någonstans att lagra information och det görs bäst i en databas med SQL. Att databaser är komplexa kommer från att kompromisser behöver göras för att göra de tillräckligt snabba samtidigt som de är tillräckligt konsistenta.

För vissa uppgifter är SQL en bra abstraktion men att använda det som generellt programmeringsspråk är att använda hammare som enda verktyg. Och då blir alla problem en spik. Synd när det också (nästan) alltid är databasen som är flaskhalsen.


Mera läsning

Databases 101

Stored Procedures as a Litmus Test

Who Needs Stored Procedures, Anyways?

Is the usage of stored procedures a bad practice?

How SQLite Helps You Do ACID


Publicerat

i

av

Etiketter: