Symuluj STWÓRZ BAZĘ DANYCH, JEŚLI NIE ISTNIEJE dla PostgreSQL?

115

Chcę utworzyć bazę danych, która nie istnieje za pośrednictwem JDBC. W przeciwieństwie do MySQL, PostgreSQL nie obsługuje create if not existsskładni. Jaki jest najlepszy sposób, aby to osiągnąć?

Aplikacja nie wie, czy baza danych istnieje, czy nie. Powinien sprawdzić i czy baza danych istnieje, należy z niej skorzystać. Dlatego warto połączyć się z żądaną bazą danych, a jeśli połączenie nie powiedzie się z powodu braku bazy danych, należy utworzyć nową bazę danych (łącząc się z domyślną postgresbazą danych). Sprawdziłem kod błędu zwrócony przez Postgres, ale nie mogłem znaleźć żadnego odpowiedniego kodu tego samego gatunku.

Inną metodą osiągnięcia tego byłoby połączenie się z postgresbazą danych i sprawdzenie, czy żądana baza danych istnieje, i podjęcie odpowiednich działań. Drugi jest trochę żmudny do wypracowania.

Czy istnieje sposób na osiągnięcie tej funkcjonalności w Postgres?

Aman Deep Gautam
źródło

Odpowiedzi:

111

Ograniczenia

Możesz zapytać o katalog systemowy pg_database- dostępny z dowolnej bazy danych w tym samym klastrze baz danych. Trudność polega na tym, że CREATE DATABASEmożna ją wykonać tylko jako pojedynczą instrukcję. Instrukcja:

CREATE DATABASE nie można wykonać wewnątrz bloku transakcji.

Dlatego nie można go uruchomić bezpośrednio w funkcji lub DOinstrukcji, gdzie byłby niejawnie wewnątrz bloku transakcji.

(Procedury SQL, wprowadzone w Postgres 11, również nie mogą w tym pomóc ).

Obejście z poziomu psql

Możesz obejść to z poziomu psql, wykonując instrukcję DDL warunkowo:

SELECT 'CREATE DATABASE mydb'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mydb')\gexec

Instrukcja:

\gexec

Wysyła bieżący bufor zapytania do serwera, a następnie traktuje każdą kolumnę każdego wiersza wyniku zapytania (jeśli istnieje) jako instrukcję SQL do wykonania.

Obejście problemu z powłoki

Z \gexecmusisz tylko raz wywołać psql :

echo "SELECT 'CREATE DATABASE mydb' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mydb')\gexec" | psql

Możesz potrzebować więcej opcji psql do połączenia; rola, port, hasło, ... Zobacz:

Tego samego nie można wywołać za pomocą, psql -c "SELECT ...\gexec"ponieważ \gexecjest to meta-polecenie psql, a -copcja oczekuje pojedynczego polecenia, dla którego podręcznik stwierdza:

commandmusi być albo ciągiem polecenia, który jest całkowicie analizowalny przez serwer (tj. nie zawiera cech charakterystycznych dla psql), albo pojedynczym poleceniem z ukośnikiem odwrotnym. Dlatego nie można łączyć meta-poleceń SQL i psql w ramach -copcji.

Obejście problemu z transakcji Postgres

Możesz użyć dblinkpołączenia z powrotem do bieżącej bazy danych, która działa poza blokiem transakcji. Dlatego też efektów nie można cofnąć.

W tym celu zainstaluj dodatkowy moduł dblink (raz na bazę danych):

Następnie:

DO
$do$
BEGIN
   IF EXISTS (SELECT FROM pg_database WHERE datname = 'mydb') THEN
      RAISE NOTICE 'Database already exists';  -- optional
   ELSE
      PERFORM dblink_exec('dbname=' || current_database()  -- current db
                        , 'CREATE DATABASE mydb');
   END IF;
END
$do$;

Ponownie, możesz potrzebować więcej opcji psql do połączenia. Zobacz dodatkową odpowiedź Ortwina:

Szczegółowe wyjaśnienie dblink:

Możesz ustawić tę funkcję jako funkcję wielokrotnego użytku.

Erwin Brandstetter
źródło
Napotkałem z tym problem podczas tworzenia bazy danych na AWS RDS Postgres z pilota. Główny użytkownik RDS nie jest superużytkownikiem i dlatego nie może z niego korzystać dblink_connect.
Ondrej Burkert,
Jeśli nie masz uprawnień administratora, możesz użyć hasła do połączenia. Szczegóły: dba.stackexchange.com/a/105186/3684
Erwin Brandstetter
Działał jak urok, używany w skrypcie init.sql wewnątrz kontenera Docker. Dzięki!
Micheal J. Roberts
Musiałem porzucić, \gexeckiedy uruchomiłem pierwsze zapytanie z powłoki, ale zadziałało.
FilBot3
117

inna alternatywa, na wypadek gdybyś chciał mieć skrypt powłoki, który tworzy bazę danych, jeśli ona nie istnieje, a poza tym po prostu zachowuje ją taką, jaka jest:

psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'my_db'" | grep -q 1 || psql -U postgres -c "CREATE DATABASE my_db"

Zauważyłem, że jest to pomocne w skryptach aprowizacji DevOps, które możesz chcieć uruchamiać wiele razy w tej samej instancji.

andreasl
źródło
Na mnie to nie działa. c:\Program Files\PostgreSQL\9.6\bin $ psql.exe -U admin -tc "SELECT 1 FROM pg_database WHERE datname = 'my_db'" | grep -q 1 || psql -U admin -c "CREATE DATABASE my_db" 'grep' is not recognized as an internal or external command, operable program or batch file.Co zrobiłem źle ?
Anton Anikeev
2
Nie masz grepna swojej drodze. W systemie Windows grepnie jest instalowany domyślnie. Możesz wyszukać gnu grep windowswersję, która może działać w systemie Windows.
Rod
Thx @Rod. Po zainstalowaniu grepa ten skrypt działał dla mnie.
Anton Anikeev
@AntonAnikeev: Można to zrobić za pomocą pojedynczego wywołania psql bez grepa. Dodałem rozwiązania do mojej odpowiedzi.
Erwin Brandstetter
1
Uważam, że warto najpierw nas pg_isready, aby sprawdzić, czy połączenie jest możliwe; jeśli połączenie nie jest dostępne (zła nazwa hosta, awaria sieci itp.), skrypt spróbuje utworzyć bazę danych i zakończy się niepowodzeniem z możliwym błędnym komunikatem o błędzie
Oliver
8

Musiałem użyć nieco rozszerzonej wersji @Erwin Brandstetter używanej:

DO
$do$
DECLARE
  _db TEXT := 'some_db';
  _user TEXT := 'postgres_user';
  _password TEXT := 'password';
BEGIN
  CREATE EXTENSION IF NOT EXISTS dblink; -- enable extension 
  IF EXISTS (SELECT 1 FROM pg_database WHERE datname = _db) THEN
    RAISE NOTICE 'Database already exists';
  ELSE
    PERFORM dblink_connect('host=localhost user=' || _user || ' password=' || _password || ' dbname=' || current_database());
    PERFORM dblink_exec('CREATE DATABASE ' || _db);
  END IF;
END
$do$

Musiałem włączyć dblinkrozszerzenie, a ponadto musiałem podać poświadczenia dla dblink. Działa z Postgres 9.4.

Ortwin Angermeier
źródło
7

Jeśli nie dbasz o dane, możesz najpierw usunąć bazę danych, a następnie utworzyć ją ponownie:

DROP DATABASE IF EXISTS dbname;
CREATE DATABASE dbname;
Andrey Semakin
źródło
Bardzo eleganckie rozwiązanie. Po prostu nie zapomnij najpierw wykonać kopii zapasowej bazy danych, jeśli zależy Ci na danych. Jednak w sytuacjach testowych jest to moje preferowane rozwiązanie.
Laryx Decidua
6

PostgreSQL nie obsługuje IF NOT EXISTSdo CREATE DATABASErachunku. Jest obsługiwany tylko w CREATE SCHEMA. Ponadto CREATE DATABASEnie może być wystawiony w transakcji, dlatego nie może być w DObloku z przechwytywaniem wyjątków.

Gdy CREATE SCHEMA IF NOT EXISTSzostanie wydany, a schemat już istnieje, zgłaszane jest powiadomienie (nie błąd) ze zduplikowanymi informacjami o obiekcie.

Aby rozwiązać te problemy, należy użyć dblinkrozszerzenia, które otwiera nowe połączenie z serwerem bazy danych i wykonuje zapytanie bez wchodzenia w transakcję. Możesz ponownie użyć parametrów połączenia, podając pusty ciąg.

Poniżej znajduje się PL/pgSQLkod, który w pełni symuluje CREATE DATABASE IF NOT EXISTSz takim samym zachowaniem jak w CREATE SCHEMA IF NOT EXISTS. Wywołuje CREATE DATABASEprzez dblink, catch duplicate_databasewyjątek (który jest wystawiany, gdy baza danych już istnieje) i przekształca go w powiadomienie z propagacją errcode. Wiadomość tekstowa została dołączona , skippingw taki sam sposób, jak to się dzieje CREATE SCHEMA IF NOT EXISTS.

CREATE EXTENSION IF NOT EXISTS dblink;

DO $$
BEGIN
PERFORM dblink_exec('', 'CREATE DATABASE testdb');
EXCEPTION WHEN duplicate_database THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;

To rozwiązanie nie ma wyścigu, jak w innych odpowiedziach, gdzie baza danych może być utworzona przez proces zewnętrzny (lub inną instancję tego samego skryptu) pomiędzy sprawdzeniem, czy baza istnieje, a jej własnym utworzeniem.

Co więcej, gdy CREATE DATABASEnie powiedzie się z innym błędem niż baza danych już istnieje, ten błąd jest propagowany jako błąd i nie jest dyskretnie odrzucany. Jest tylko haczyk na duplicate_databasebłąd. Więc naprawdę zachowuje się tak, jak IF NOT EXISTSpowinien.

Możesz umieścić ten kod we własnej funkcji, wywołać go bezpośrednio lub z transakcji. Samo wycofanie (przywrócenie usuniętej bazy danych) nie zadziała.

Wyjście testowe (wywoływane dwa razy przez DO, a następnie bezpośrednio):

$ sudo -u postgres psql
psql (9.6.12)
Type "help" for help.

postgres=# \set ON_ERROR_STOP on
postgres=# \set VERBOSITY verbose
postgres=# 
postgres=# CREATE EXTENSION IF NOT EXISTS dblink;
CREATE EXTENSION
postgres=# DO $$
postgres$# BEGIN
postgres$# PERFORM dblink_exec('', 'CREATE DATABASE testdb');
postgres$# EXCEPTION WHEN duplicate_database THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
DO
postgres=# 
postgres=# CREATE EXTENSION IF NOT EXISTS dblink;
NOTICE:  42710: extension "dblink" already exists, skipping
LOCATION:  CreateExtension, extension.c:1539
CREATE EXTENSION
postgres=# DO $$
postgres$# BEGIN
postgres$# PERFORM dblink_exec('', 'CREATE DATABASE testdb');
postgres$# EXCEPTION WHEN duplicate_database THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
NOTICE:  42P04: database "testdb" already exists, skipping
LOCATION:  exec_stmt_raise, pl_exec.c:3165
DO
postgres=# 
postgres=# CREATE DATABASE testdb;
ERROR:  42P04: database "testdb" already exists
LOCATION:  createdb, dbcommands.c:467
Pali
źródło
1
Jest to obecnie jedyna poprawna odpowiedź, która nie cierpi z powodu warunków wyścigu i wykorzystuje niezbędną selektywną obsługę błędów. Szkoda, że ​​ta odpowiedź pojawiła się po (nie do końca poprawnej) odpowiedzi u góry, która zebrała ponad 70 punktów.
vog,
2
Cóż, inne odpowiedzi nie są tak dokładne, aby poradzić sobie ze wszystkimi możliwymi przypadkami narożnymi, które mogą się zdarzyć. Możesz również wywoływać mój kod PL / pgSQL więcej razy równolegle i nie zawodzi.
Pali
1

Jeśli możesz użyć powłoki, spróbuj

psql -U postgres -c 'select 1' -d $DB &>dev/null || psql -U postgres -tc 'create database $DB'

Myślę, że psql -U postgres -c "select 1" -d $DBjest łatwiejszy niż SELECT 1 FROM pg_database WHERE datname = 'my_db'i potrzebuje tylko jednego rodzaju cytatu, łatwiejszego do połączenia sh -c.

Używam tego w moim zadaniu ansibla

- name: create service database
  shell: docker exec postgres sh -c '{ psql -U postgres -tc "SELECT 1" -d {{service_name}} &> /dev/null && echo -n 1; } || { psql -U postgres -c "CREATE DATABASE {{service_name}}"}'
  register: shell_result
  changed_when: "shell_result.stdout != '1'"
wener
źródło
0

Wystarczy stworzyć bazę danych za pomocą createdbnarzędzia CLI:

PGHOST="my.database.domain.com"
PGUSER="postgres"
PGDB="mydb"
createdb -h $PGHOST -p $PGPORT -U $PGUSER $PGDB

Jeśli baza danych istnieje, zwróci błąd:

createdb: database creation failed: ERROR:  database "mydb" already exists
James Wierzba
źródło