Esperienza 'Database as Code'

Esperienza 'Database as Code'

SQL, cosa può essere più semplice? Ognuno di noi può scrivere una semplice query — digitando select, elencando le colonne necessarie, poi da, il nome della tabella, qualche condizione in where e voilà — i dati utili sono nella nostra tasca, quasi indipendentemente dal tipo di DBMS che stiamo utilizzando nel backend (o forse non è nemmeno un DBMS) non è affatto un DBMS). Di conseguenza, il lavoro con praticamente qualsiasi fonte di dati (relazionali e non) può essere considerato dal punto di vista di un normale codice (con tutte le conseguenze — controllo delle versioni, revisione del codice, analisi statica, test automatici e tutto il resto). E questo riguarda non solo i dati stessi, gli schemi e le migrazioni, ma in generale l'intera vita del database. In questo articolo parleremo delle attività quotidiane e dei problemi che comporta lavorare con vari DB sotto il focus "database as code".

E iniziamo subito con ORM. I primi scontri del tipo "SQL vs ORM" furono notati già nella Russia prerivoluzionaria.

Il mapping oggetto-relazionale

I sostenitori dell'ORM tradizionalmente apprezzano la velocità e la semplicità di sviluppo, l'indipendenza dal DBMS e la pulizia del codice. Per molti di noi, il codice per lavorare con il database (e spesso anche il database stesso)

di solito appare più o meno così…

@Entity
@Table(name = "stock", catalog = "maindb", uniqueConstraints = {
        @UniqueConstraint(columnNames = "STOCK_NAME"),
        @UniqueConstraint(columnNames = "STOCK_CODE") })
public class Stock implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "STOCK_ID", unique = true, nullable = false)
    public Integer getStockId() {
        return this.stockId;
    }
  ...

Il modello è ricco di annotazioni intelligenti, mentre dietro le quinte il valoroso ORM genera ed esegue tonnellate di codice SQL. A proposito, gli sviluppatori cercano in tutti i modi di isolarsi dal loro database attraverso chilometri di astrazioni, il che indica una certa "odio per SQL".

Dall'altra parte della barricata, i sostenitori del "SQL fatto a mano" sottolineano la possibilità di spremere ogni goccia dal proprio DBMS senza strati e astrazioni aggiuntivi. Questo porta alla nascita di progetti "data-centric", dove a curare il database sono persone appositamente formate (chiamate anche "database specialisti", "database engineers", "database developers" ecc.), mentre agli sviluppatori rimane solo il compito di "tirare" le viste e le procedure già pronte, senza entrare nei dettagli.

E se prendessimo il meglio di entrambi i mondi? Come è stato fatto in questo straordinario strumento con un nome che dà fiducia, Yesql. Riporto un paio di frasi dalla concettualizzazione generale in una mia libera traduzione, e per una panoramica più dettagliata, si può approfondire qui.

Clojure è un linguaggio fantastico per creare DSL, ma SQL è già di per sé un fantastico DSL, e non abbiamo bisogno di un altro. Le S-espressioni sono meravigliose, ma qui non aggiungono nulla di nuovo. In definitiva, otteniamo parentesi per il semplice fatto di avere parentesi. Non sei d'accordo? Aspetta solo il momento in cui l'astrazione sul DB inizierà a perdere colpi, e tu inizierai a combattere con la funzione (raw-sql)

E ora? Lasciamo SQL com'è, un semplice SQL: un file per una query:

-- name: users-by-country
select *
  from users
 where country_code = :country_code

… e poi leggi questo file, trasformandolo in una normale funzione Clojure:

(defqueries "some/where/users_by_country.sql"
   {:connection db-spec})

;;; È stata creata una funzione con il nome `users-by-country`.
;;; Utilizziamola:
(users-by-country {:country_code "GB"})
;=> ({:name "Kris" :country_code "GB" ...} ...)

Adottando il principio "SQL separato, Clojure separato", ottieni:

  • Nessuna sorpresa sintattica. Il tuo database (come qualsiasi altro) non è conforme al 100% allo standard SQL — ma per Yesql non è un problema. Non perderai mai tempo a cercare funzioni con una sintassi equivalente a SQL. Non dovrai mai tornare alla funzione (raw-sql "some (‘funky’ :: SYNTAX)")).
  • Miglior supporto dell'editor. Il tuo editor già ha un ottimo supporto per SQL. Mantenendo SQL come SQL, puoi semplicemente utilizzarlo.
  • Compatibilità con il team. I tuoi DBA possono leggere e scrivere SQL che usi nel tuo progetto Clojure.
  • Impostazione delle prestazioni più semplice. Devi costruire un piano per una query problematica? Non è un problema quando la tua query è un semplice SQL.
  • Riutilizzo delle query. Puoi trascinare questi stessi file SQL in altri progetti, perché è solo un vecchio buon SQL — basta condividerlo.

A mio avviso, l'idea è molto interessante e allo stesso tempo molto semplice, il che ha portato il progetto ad avere molti seguaci in diversi linguaggi. E noi cercheremo di applicare una filosofia simile di separazione del codice SQL da tutto il resto, molto oltre l'ORM.

IDE & gestori di DB

Iniziamo con un compito quotidiano semplice. Spesso dobbiamo cercare oggetti nel database, ad esempio, trovare una tabella in uno schema e studiarne la struttura (quali colonne, chiavi, indici, vincoli e altro vengono utilizzati). E da qualsiasi IDE grafica o anche da un DB manager modesto, ci aspettiamo in primis queste capacità. Perché sia veloce e non dobbiamo attendere mezz'ora affinché appaia la finestra con le informazioni necessarie (soprattutto con una connessione lenta a un database remoto), e nel contempo, le informazioni ottenute devono essere fresche e pertinenti, non un vecchio materiale memorizzato in cache. E più complesso e grande è il database e maggiore è il numero di essi, più difficile diventa fare ciò.

Ma di solito metto il mouse da parte e scrivo semplicemente il codice. Supponiamo che sia necessario scoprire quali tabelle (e con quali proprietà) sono contenute nello schema "HR". Nella maggior parte dei DBMS, è possibile ottenere il risultato desiderato con una semplice query da information_schema:

select table_name
     , ...
  from information_schema.tables
 where schema = 'HR'

Da un database all'altro, il contenuto di tali tabelle di riferimento varia a seconda delle capacità di ogni DBMS. E, ad esempio, per MySQL, dallo stesso catalogo è possibile ottenere parametri specifici per questo DBMS della tabella:

select table_name
     , storage_engine -- Motore utilizzato ("MyISAM", "InnoDB" ecc.)
     , row_format     -- Formato della riga ("Fixed", "Dynamic" ecc.)
     , ...
  from information_schema.tables
 where schema = 'HR'

Oracle non supporta information_schema, ma ha invece metadati Oracle, e non ci sono grandi problemi:

select table_name
     , pct_free       -- Percentuale minima di spazio libero nel blocco dati (%)
     , pct_used       -- Percentuale minima di spazio utilizzato nel blocco dati (%)
     , last_analyzed  -- Data dell'ultimo raccolto di statistiche
     , ...
  from all_tables
 where owner = 'HR'

Non è da meno ClickHouse:

select name
     , engine -- Motore utilizzato ("MergeTree", "Dictionary" ecc.)
     , ...
  from system.tables
 where database = 'HR'

Qualcosa di simile può essere fatto anche in Cassandra (dove ci sono columnfamilies invece di tabelle e keyspace al posto degli schemi):

select columnfamily_name
     , compaction_strategy_class  -- Strategia di compattazione
     , gc_grace_seconds           -- Tempo di vita dei dati spazzatura
     , ...
  from system.schema_columnfamilies
 where keyspace_name = 'HR'

Per la maggior parte degli altri DB, è possibile pensare a query simili (anche in Mongo c'è una collezione sistematica speciale, che contiene informazioni su tutte le collezioni nel sistema).

Naturalmente, in questo modo è possibile ottenere informazioni non solo sulle tabelle, ma su qualsiasi oggetto. Periodicamente, persone gentilissime condividono questo tipo di codice per vari DB, come ad esempio nella serie di articoli di Habr "Funzioni per la documentazione dei database PostgreSQL" (aib, ben, gim). Naturalmente, tenere a mente tutta questa montagna di query e digitarle continuamente è un "piacere" piuttosto relativo, quindi nel mio IDE/editor preferito ho un insieme di snippet preparati in anticipo per le query usate frequentemente, e resta solo da inserire i nomi degli oggetti nel modello.

Di conseguenza, questo modo di navigare e cercare oggetti è molto più flessibile, fa risparmiare molto tempo e consente di ottenere proprio le informazioni necessarie e nel formato richiesto (come spiegato nel post "Esportazione dei dati da DB in qualsiasi formato: cosa sanno fare gli IDE sulla piattaforma IntelliJ").

Operazioni con gli oggetti

Dopo aver trovato e studiato gli oggetti necessari, è il momento di fare qualcosa di utile con essi. Naturalmente, senza staccare le dita dalla tastiera.

Non è un segreto che la semplice eliminazione di una tabella appare praticamente identica in quasi tutti i DB:

drop table hr.persons

Ma la creazione di una tabella è già più interessante. Quasi tutti i DBMS (compresi molti NoSQL) sanno in un modo o nell'altro come usare "create table", e la parte principale di questo comando sarà poco diversa (nome, elenco delle colonne, tipi di dati), ma gli altri dettagli possono differire ampiamente e dipendono dalla struttura interna e dalle funzionalità specifiche di ciascun DBMS. Il mio esempio preferito è che nella documentazione Oracle solo le "strutture" BNF per la sintassi di "create table" occupano 31 pagine. Altri DBMS hanno funzionalità più modeste, ma ognuno di essi ha anche molte caratteristiche interessanti e uniche per la creazione di tabelle (postgres, mysql, cockroach, cassandra). È improbabile che qualche "wizard" grafico di un altro IDE (soprattutto se universale) possa coprire completamente tutte queste capacità, e se lo fa, sarà uno spettacolo non per i deboli di cuore. D'altra parte, un operatore ben scritto create table permetterà di sfruttare facilmente tutte queste funzionalità, rendendo l'archiviazione e l'accesso ai vostri dati affidabili, ottimali e il più confortevoli possibile.

In molte SGBD esistono anche tipi di oggetti specifici, che mancano in altre SGBD. Possiamo eseguire operazioni non solo sugli oggetti del database, ma anche sullo stesso SGBD, ad esempio "terminare" un processo, liberare una certa area di memoria, attivare il tracing, passare alla modalità "sola lettura" e molto altro.

Ora facciamo un po' di disegno

Una delle attività più comuni è costruire un diagramma con gli oggetti del database, vedere in un'immagine bella gli oggetti e le relazioni tra di essi. Questo può essere fatto praticamente da qualsiasi IDE grafica, da utilità "da riga di comando" specifiche, strumenti grafici specializzati e modellatori. Questi strumenti fanno un po' di disegno "come sanno", e si può influenzare questo processo solo usando alcuni parametri nel file di configurazione o spuntando opzioni nell'interfaccia.

Tuttavia, questo problema può essere risolto in modo molto più semplice, flessibile ed elegante, e naturalmente con l'uso di codice. Per costruire diagrammi di qualsiasi complessità, abbiamo immediatamente diversi linguaggi di markup specializzati (DOT, GraphML, ecc.), e a essi si affiancano numerose applicazioni (GraphViz, PlantUML, Mermaid), che possono leggere tali istruzioni e visualizzarle in vari formati. Già sappiamo come ottenere le informazioni sugli oggetti e sulle relazioni tra di essi.

Facciamo un piccolo esempio di come potrebbe apparire, utilizzando PlantUML e un database dimostrativo per PostgreSQL (a sinistra la query SQL che genererà l'istruzione necessaria per PlantUML, a destra il risultato):

Esperienza 'Database as Code'

select '@startuml'||chr(10)||'hide methods'||chr(10)||'hide stereotypes' union all
select distinct ccu.table_name || ' --|>' ||
       tc.table_name as val
  from table_constraints as tc
  join key_column_usage as kcu
    on tc.constraint_name = kcu.constraint_name
  join constraint_column_usage as ccu
    on ccu.constraint_name = tc.constraint_name
 where tc.constraint_type = 'FOREIGN KEY'
   and tc.table_name ~ '.*' union all
select '@enduml'

E se si presta un po' di attenzione, sulla base del template ER per PlantUML si può ottenere qualcosa di molto simile a un vero diagramma ER:

La query SQL è un po' più complessa

-- Intestazione
select '@startuml
        !define Table(nome,desc) class nome as "desc" << (T,#FFAAAA) >&gt;
        !define primary_key(x) <b>x</b>
        !define unique(x) <color:green>x</color>
        !define not_null(x) <u>x</u>
        hide methods
        hide stereotypes'
 union all
-- Tabelle
select format('Table(%s, "%s n informazioni su %s") {'||chr(10), table_name, table_name, table_name) ||
       (select string_agg(column_name || ' ' || upper(udt_name), chr(10))
          from information_schema.columns
         where table_schema = 'public'
           and table_name = t.table_name) || chr(10) || '}'
  from information_schema.tables t
 where table_schema = 'public'
 union all
-- Relazioni tra le tabelle
select distinct ccu.table_name || ' "1" --&gt; "0..N" ' || tc.table_name || format(' : "A %s può avere molti %s"', ccu.table_name, tc.table_name)
  from information_schema.table_constraints as tc
  join information_schema.key_column_usage as kcu on tc.constraint_name = kcu.constraint_name
  join information_schema.constraint_column_usage as ccu on ccu.constraint_name = tc.constraint_name
 where tc.constraint_type = 'FOREIGN KEY'
   and ccu.constraint_schema = 'public'
   and tc.table_name ~ '.*'
 union all
-- Feed
select '@enduml'

Esperienza 'Database as Code'

Se si guarda attentamente, si vedrà che molti strumenti di visualizzazione utilizzano query simili. Tuttavia, queste query sono solitamente profondamente "incorporate" nel codice stesso dell'applicazione e difficili da comprendere, per non parlare di qualsiasi modifica.

Metriche e monitoraggio

Passiamo a un tema tradizionalmente complesso: il monitoraggio delle prestazioni del database. Vorrei ricordare una piccola storia vera, raccontata da "un mio amico". In un progetto, viveva un potente DBA, e pochi sviluppatori lo conoscevano di persona o lo avevano mai visto di persona (nonostante si dicesse che lavorasse, a quanto pare, in un edificio vicino). Al momento "X", quando il sistema di produzione di un grande rivenditore iniziava a "star male" per l'ennesima volta, lui silenziosamente inviava screenshot dei grafici dall'Enterprise Manager di Oracle, evidenziando con un evidenziatore rosso i punti critici per "chiarezza" (cosa che, per usare un eufemismo, aiutava poco). E così bisognava trovare una soluzione basandosi su questa "fotogallery". Nessuno aveva accesso all'Enterprise Manager, prezioso (in entrambi i sensi della parola), poiché il sistema era complesso e costoso, e c'era il timore che "gli sviluppatori potessero combinare guai e rompere tutto". Così gli sviluppatori individuavano, attraverso un approccio "empirico", la causa dei rallentamenti e lanciavano una patch. Se non arrivava nuovamente una lettera minacciosa dal DBA nel frattempo, tutti tiravano un sospiro di sollievo e tornavano ai loro compiti attuali (fino alla prossima Letterina).

Ma il processo di monitoraggio può apparire più divertente e amichevole, e soprattutto — accessibile e trasparente per tutti. Anche solo la sua parte base, come integrazione ai sistemi di monitoraggio principali (che sono senza dubbio utili e in molti casi indispensabili). Qualsiasi DBMS è pronto a condividere liberamente e gratuitamente informazioni sul proprio stato attuale e sulle prestazioni. Nella stessa "terribile" Oracle DB, è possibile ottenere praticamente qualsiasi informazione sulle prestazioni da viste di sistema, dai processi e sessioni fino allo stato della cache dei buffer (ad esempio, Script DBA, sezione "Monitoraggio"). In Postgresql ci sono anche molte viste di sistema per il monitoraggio del lavoro del database, in particolare quelle indispensabili nella vita quotidiana di qualsiasi DBA, come pg_stat_activity, pg_stat_database, pg_stat_bgwriter. In MySQL è stata persino creata una specifica schema performance_schema. In Mongo, il profilatore aggregando dati sulle prestazioni in una collezione di sistema system.profile.

Pertanto, armati di un qualsiasi raccoglitore di metriche (Telegraf, Metricbeat, Collectd), che può eseguire query SQL personalizzate, di un deposito per queste metriche (InfluxDB, Elasticsearch, Timescaledb) e di un visualizzatore (Grafana, Kibana), è possibile ottenere un sistema di monitoraggio piuttosto semplice e flessibile, che sarà strettamente integrato con altre metriche di sistema (ottenute, ad esempio, dal server delle applicazioni, dal sistema operativo, ecc.). Come, ad esempio, è stato fatto in pgwatch2, dove viene utilizzata la combinazione InfluxDB + Grafana e un insieme di query alle viste di sistema, a cui è possibile anche aggiungere query personalizzate.

Totale

E questo è solo un elenco approssimativo di ciò che si può fare con il nostro database tramite codice SQL standard. Sono sicuro che si possono trovare molte altre applicazioni, scrivetelo nei commenti. E parleremo di come (e, soprattutto, perché) automatizzare tutto questo e includerlo nel proprio pipeline CI/CD la prossima volta.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster