16/07/2021
Francesco Gnarra
Utilizzo di Database multipli in una singola query nelle app Rails
Immaginiamo di voler creare un'applicazione separata per il nostro sistema di e-commerce dedicata alla Business Intelligence. In altre parole, vogliamo calcolare alcune statistiche per gli ordini ricevuti. Creeremo quindi un nuovo modello, ad esempio OrderStat, e avremo un database Postgres separato per una nuova app. Sembra facile, ma: come possiamo ottenere i dati dagli ordini effettivi? Un modo per farlo sarebbe quello di non accedere direttamente ai dati e trasmettere tutti gli ordini (o gli eventi relativi agli ordini) in Kafka e lasciare che i consumatori ottengano i dati dalla stessa piattaforma.
D'altra parte, siamo passati dall'interrogare una singola tabella da un database diverso, che sembra una cosa semplice, a un ecosistema guidato da eventi supportato da Kafka, che è una cosa complessa, soprattutto se non abbiamo molta esperienza con esso. Questa volta, per tale particolare problema, esploreremo una soluzione che forse non è “bella” stilisticamente, ma fa il suo lavoro in modo efficiente - eseguendo query tra due database PostgreSQL separati (inclusi i join!). Come? Utilizzando i Foreign Data Wrappers.
FOREIGN DATA WRAPPERS
Foreign Data Wrappers (FDW) è un’ottima funzionalità di PostgreSQL che ci consente di eseguire query su origini dati esterne. L'origine dati esterna non è solo un database Postgres diverso: potrebbe essere qualsiasi cosa purché sia disponibile l'estensione appropriata per quella particolare origine dati. È possibile farlo funzionare con MySQL, Redis, MongoDB e persino Kafka, quindi la flessibilità è piuttosto impressionante. Tuttavia, concentriamoci sull'integrazione Postgres-to-Postgres, che è disponibile immediatamente.
L'idea alla base dei FDW è abbastanza semplice: dopo aver abilitato l'estensione, dobbiamo definire un server esterno, la mappatura di come accedere a quel server e creare tabelle esterne, che sono simili a adattatori/proxy per un'origine dati esterna. Alla fine, quello che faremo sarà semplicemente eseguire le query su un'altra tabella: sarà solo una con alcuni extra rispetto a una standard.
Definite le basi, vediamo come potremmo utilizzarlo in un'applicazione Rails.
UTILIZZO IN RAILS
Immaginiamo di avere un modello OrderStat nella nostra app attuale e di aver bisogno di alcuni dati dal modello Order rappresentato dalla tabella "ordini" da un database diverso.
Avremo bisogno di quattro migration per farlo funzionare.
Per prima cosa, creiamo l'estensione:
class CreateFdwExtension < ActiveRecord::Migration[6.1]
def up
execute "CREATE EXTENSION postgres_fdw;"
end
def down
execute "DROP EXTENSION postgres_fdw;"
end
end
Successivamente, creiamo un server:
class CreateFdwServer < ActiveRecord::Migration[6.1]
def up
execute "CREATE SERVER server_name
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', dbname 'name_of_external_db');
"
end
def down
execute "DROP SERVER server_name"
end
end
Nel prossimo step, avremo bisogno di fornire username e password per accedere al DB:
class CreateFdwMapping < ActiveRecord::Migration[6.1]
def up
execute "CREATE USER MAPPING FOR CURRENT_USER SERVER name_of_external_db OPTIONS (user '', password '');"
end
def down
execute "DROP USER MAPPING FOR CURRENT_USER SERVER name_of_external_db"
end
end
Nell'ultimo passaggio, creeremo una tabella esterna. Un modo per farlo è tramite gli ordini CREATE FOREIGN TABLE in cui si fornisce lo schema esatto per questa tabella; tuttavia, tale operazione risulta essere non efficiente per un numero elevato di colonne. È molto più comodo usare IMPORT FOREIGN SCHEMA dove possiamo fornire il nome dello schema (a meno che non abbiamo scelto una soluzione personalizzata, usiamo semplicemente "public"), il nome delle tabelle e il nome del server, e il gioco è fatto! Non è necessario preoccuparsi delle colonne esatte e dei relativi tipi e vincoli.
class CreateForeignAccountsTable < ActiveRecord::Migration[6.1]
def up
execute "IMPORT FOREIGN SCHEMA public LIMIT TO (orders) FROM SERVER server_name INTO public;"
# In alternativa:
# execute "CREATE FOREIGN TABLE orders (
# id integer NOT NULL
# )
# SERVER server_name
# OPTIONS (schema_name 'public', table_name 'orders');
# "
end
def down
execute "DROP FOREIGN TABLE orders"
end
end
E questo è tutto!
Possiamo testare una join nel seguente modo:
# assuming that OrderStat and Order models exist in the app and OrderStat belongs to Order
OrderStat.joins(:order)
Ed è così che utilizziamo le tabelle da due diversi database. Tuttavia, per farlo funzionare completamente nella nostra applicazione Rails, in modo che possiamo, ad esempio, eseguire query semplici come OrderStat.joins(:order).first.order, potremmo aver bisogno di una configurazione nel modello Order specificando esplicitamente la primary key; in caso contrario, potremmo ricevere il seguente errore:
ActiveRecord::UnknownPrimaryKey (Unknown primary key for table orders in model Order.)
Dunque:
class Order
<
ApplicationRecord
self.primary_key
=
"id"
end
E il gioco è fatto!
AGGIORNAMENTO DELLO SCHEMA
E' molto probabile che lo schema della tabella "ordini" potrà cambiare. In tal caso, se è necessario aggiornare lo schema è sufficiente ricreare le tabelle esterne.
Probabilmente l'esecuzione di query tra due diversi database, in particolare l'esecuzione di join, non è qualcosa che si fa tutti i giorni e, in una certa misura, potrebbe indicare una modifica impattante all'architettura del nostro software.
Tuttavia, per tale scopo, ci vengono incontro Postgres Foreign Data Wrappers che, come abbiamo visto, sono semplicissimi da utilizzare.