PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

The "O" di ORDBMS:Warisan PostgreSQL

Dalam entri blog ini kita akan membahas warisan PostgreSQL, yang secara tradisional merupakan salah satu fitur teratas PostgreSQL sejak rilis awal. Beberapa penggunaan warisan di PostgreSQL adalah:

  • partisi tabel
  • multi-penyewaan

PostgreSQL hingga versi 10 mengimplementasikan partisi tabel menggunakan pewarisan. PostgreSQL 10 menyediakan cara baru untuk partisi deklaratif. Partisi PostgreSQL menggunakan pewarisan adalah teknologi yang cukup matang, didokumentasikan dan diuji dengan baik, namun pewarisan di PostgreSQL dari perspektif model data (menurut saya) tidak begitu luas, oleh karena itu kami akan berkonsentrasi pada kasus penggunaan yang lebih klasik di blog ini. Kami melihat dari blog sebelumnya (opsi multi-penyewaan untuk PostgreSQL) bahwa salah satu metode untuk mencapai multi-penyewaan adalah dengan menggunakan tabel terpisah dan kemudian menggabungkannya melalui tampilan. Kami juga melihat kekurangan dari desain ini. Di blog ini kami akan menyempurnakan desain ini menggunakan pewarisan.

Pengantar Warisan

Melihat kembali metode multi-tenancy yang diterapkan dengan tabel dan tampilan terpisah, kami ingat bahwa kelemahan utamanya adalah ketidakmampuan untuk melakukan penyisipan/pembaruan/penghapusan. Saat kami mencoba pembaruan di sewa lihat kita akan mendapatkan ERROR ini:

ERROR:  cannot insert into view "rental"
DETAIL:  Views containing UNION, INTERSECT, or EXCEPT are not automatically updatable.
HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.

Jadi, kita perlu membuat pemicu atau aturan di sewa view yang menentukan fungsi untuk menangani insert/update/delete. Alternatifnya adalah menggunakan warisan. Mari kita ubah skema blog sebelumnya:

template1=# create database rentaldb_hier;
template1=# \c rentaldb_hier
rentaldb_hier=# create schema boats;
rentaldb_hier=# create schema cars;

Sekarang mari kita buat tabel induk utama:

rentaldb_hier=# CREATE TABLE rental (
    id integer NOT NULL,
    customerid integer NOT NULL,
    vehicleno text,
    datestart date NOT NULL,
    dateend date
); 

Dalam istilah OO tabel ini sesuai dengan superclass (dalam terminologi java). Sekarang mari kita definisikan tabel anak dengan mewarisi dari public.rental dan juga menambahkan kolom untuk setiap tabel yang spesifik untuk domain:mis. nomor SIM wajib (pelanggan) untuk mobil dan sertifikat opsional berlayar perahu.

rentaldb_hier=# create table cars.rental(driv_lic_no text NOT NULL) INHERITS (public.rental);
rentaldb_hier=# create table boats.rental(sail_cert_no text) INHERITS (public.rental);

Dua meja cars.rental dan boats.rental mewarisi semua kolom dari induknya public.rental :
 

rentaldb_hier=# \d cars.rental
                           Table "cars.rental"
     Column     |         Type          | Collation | Nullable | Default
----------------+-----------------------+-----------+----------+---------
 id             | integer               |           | not null |
 customerid     | integer               |           | not null |
 vehicleno      | text                  |           |          |
 datestart      | date                  |           | not null |
 dateend        | date                  |           |          |
 driv_lic_no | text                  |           | not null |
Inherits: rental
rentaldb_hier=# \d boats.rental
                         Table "boats.rental"
    Column    |         Type          | Collation | Nullable | Default
--------------+-----------------------+-----------+----------+---------
 id           | integer               |           | not null |
 customerid   | integer               |           | not null |
 vehicleno    | text                  |           |          |
 datestart    | date                  |           | not null |
 dateend      | date                  |           |          |
 sail_cert_no | text                  |           |          |
Inherits: rental

Kami melihat bahwa kami menghilangkan perusahaan kolom dalam definisi tabel induk (dan sebagai konsekuensinya juga dalam tabel anak). Ini tidak lagi diperlukan karena identifikasi penyewa ada di nama lengkap tabel! Nanti kita akan melihat cara mudah untuk menemukan ini dalam kueri. Sekarang mari kita masukkan beberapa baris ke dalam tiga tabel (kita meminjam pelanggan skema dan data dari blog sebelumnya):

rentaldb_hier=# insert into rental (id, customerid, vehicleno, datestart) VALUES(1,1,'SOME ABSTRACT PLATE NO',current_date);
rentaldb_hier=# insert into cars.rental (id, customerid, vehicleno, datestart,driv_lic_no) VALUES(2,1,'INI 8888',current_date,'gr690131');
rentaldb_hier=# insert into boats.rental (id, customerid, vehicleno, datestart) VALUES(3,2,'INI 9999',current_date);

Sekarang mari kita lihat apa yang ada di tabel:

rentaldb_hier=# select * from rental ;
 id | customerid |       vehicleno        | datestart  | dateend
----+------------+------------------------+------------+---------
  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
  2 |          1 | INI 8888               | 2018-08-31 |
  3 |          2 | INI 9999               | 2018-08-31 |
(3 rows)
rentaldb_hier=# select * from boats.rental ;
 id | customerid | vehicleno | datestart  | dateend | sail_cert_no
----+------------+-----------+------------+---------+--------------
  3 |          2 | INI 9999  | 2018-08-31 |         |
(1 row)
rentaldb_hier=# select * from cars.rental ;
 id | customerid | vehicleno | datestart  | dateend | driv_lic_no
----+------------+-----------+------------+---------+-------------
  2 |          1 | INI 8888  | 2018-08-31 |         | gr690131
(1 row)

Jadi pengertian pewarisan yang sama yang ada dalam bahasa Berorientasi Objek (seperti Java) juga ada di PostgreSQL! Kita dapat memikirkannya sebagai berikut:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instance dari public.rental
row cars.rental.id =2:instance cars.rental dan public.rental
row boats.rental.id =3:instance boats.rental dan public.rental

Karena deretan perahu. rental dan mobil. rental juga merupakan contoh dari public.rental, wajar jika mereka muncul sebagai deretan public.rental. Jika kita ingin hanya baris eksklusif dari public.rental (dengan kata lain baris dimasukkan langsung ke public.rental) kita melakukannya dengan menggunakan kata kunci HANYA sebagai berikut:

rentaldb_hier=# select * from ONLY rental ;
 id | customerid |       vehicleno        | datestart  | dateend
----+------------+------------------------+------------+---------
  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
(1 row)

Satu perbedaan antara Java dan PostgreSQL dalam hal pewarisan adalah:Java tidak mendukung pewarisan berganda sementara PostgreSQL mendukung, dimungkinkan untuk mewarisi dari lebih dari satu tabel, jadi dalam hal ini kita mungkin menganggap tabel lebih seperti antarmuka di Jawa.

Jika kita ingin mengetahui tabel yang tepat dalam hierarki tempat baris tertentu berada (setara dengan obj.getClass().getName() di java) kita dapat melakukannya dengan menentukan kolom khusus tableoid (oid dari masing-masing tabel di kelas pg ), dicor ke regclass yang memberikan nama tabel lengkap:

rentaldb_hier=# select tableoid::regclass,* from rental ;
   tableoid   | id | customerid |       vehicleno        | datestart  | dateend
--------------+----+------------+------------------------+------------+---------
 rental       |  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
 cars.rental  |  2 |          1 | INI 8888               | 2018-08-31 |
 boats.rental |  3 |          2 | INI 9999               | 2018-08-31 |
(3 rows)

Dari tabel di atas (tableoid berbeda) kita dapat menyimpulkan bahwa tabel dalam hierarki hanyalah tabel PostgreSQL lama, yang terhubung dengan hubungan pewarisan. Tapi selain itu, mereka bertindak seperti tabel biasa. Dan ini akan ditekankan lebih lanjut di bagian berikut.

Fakta &Peringatan Penting Tentang Warisan PostgreSQL

Tabel anak mewarisi:

  • BUKAN batasan NULL
  • PERIKSA batasan

Tabel anak TIDAK mewarisi:

  • Kendala KUNCI UTAMA
  • Batasan UNIK
  • Kendala KUNCI ASING

Ketika kolom dengan nama yang sama muncul pada definisi lebih dari satu tabel pada hierarki, maka kolom tersebut harus memiliki tipe yang sama dan digabungkan menjadi satu kolom. Jika batasan NOT NULL ada untuk nama kolom di mana pun dalam hierarki, maka ini diwarisi ke tabel anak. PERIKSA batasan dengan nama yang sama juga digabungkan dan harus memiliki kondisi yang sama.

Perubahan skema pada tabel induk (melalui ALTER TABLE) disebarkan ke seluruh hierarki yang ada di bawah tabel induk ini. Dan ini adalah salah satu fitur bagus dari pewarisan di PostgreSQL.

Kebijakan Keamanan dan Keamanan (RLS) diputuskan berdasarkan tabel aktual yang kami gunakan. Jika kita menggunakan tabel induk maka Keamanan dan RLS tabel tersebut akan digunakan. Tersirat bahwa pemberian hak istimewa pada tabel induk memberikan izin ke tabel anak juga, tetapi hanya bila diakses melalui tabel induk. Untuk mengakses tabel anak secara langsung, maka kita harus memberikan GRANT eksplisit langsung ke tabel anak, hak istimewa pada tabel induk tidak akan cukup. Hal yang sama berlaku untuk RLS.

Mengenai pengaktifan pemicu, pemicu tingkat pernyataan bergantung pada tabel bernama pernyataan, sedangkan pemicu tingkat baris akan diaktifkan bergantung pada tabel tempat baris sebenarnya (sehingga bisa berupa tabel anak).

Hal-hal yang harus diperhatikan:

  • Sebagian besar perintah bekerja di seluruh hierarki dan mendukung notasi HANYA. Namun, beberapa perintah tingkat rendah (REINDEX, VACUUM, dll) hanya berfungsi pada tabel fisik yang diberi nama oleh perintah tersebut. Pastikan untuk membaca dokumentasi setiap kali jika ada keraguan.
  • Batasan KUNCI ASING (tabel induk berada di sisi referensi) tidak diwariskan. Ini mudah diselesaikan dengan menentukan batasan FK yang sama di semua tabel turunan dari hierarki.
  • Sampai saat ini (PostgreSQL 10), tidak ada cara untuk memiliki INDEKS UNIK global (KUNCI UTAMA atau batasan UNIK) pada sekelompok tabel. Akibatnya:
    • KUNCI UTAMA dan batasan UNIK tidak diwariskan, dan tidak ada cara mudah untuk menerapkan keunikan pada kolom di semua anggota hierarki
    • Bila tabel induk berada di sisi referensi dari batasan KUNCI ASING, maka pemeriksaan hanya dilakukan untuk nilai kolom pada baris yang benar-benar (secara fisik) milik tabel induk, bukan tabel anak.

Batasan terakhir adalah yang serius. Menurut dokumen resmi tidak ada solusi yang baik untuk ini. Namun, FK dan keunikan sangat mendasar untuk desain basis data yang serius. Kami akan mencari cara untuk mengatasinya.

Unduh Whitepaper Hari Ini Pengelolaan &Otomatisasi PostgreSQL dengan ClusterControlPelajari tentang apa yang perlu Anda ketahui untuk menerapkan, memantau, mengelola, dan menskalakan PostgreSQLUnduh Whitepaper

Pewarisan dalam Praktek

Pada bagian ini, kita akan mengubah desain klasik dengan tabel biasa, batasan PRIMARY KEY/UNIQUE dan FOREIGN KEY, menjadi desain multi-tenant berdasarkan pewarisan dan kita akan mencoba menyelesaikan masalah (yang diharapkan sesuai dengan bagian sebelumnya) yang kita wajah. Mari kita perhatikan bisnis rental yang sama dengan yang kita gunakan sebagai contoh di blog sebelumnya dan bayangkan pada awalnya bisnis ini hanya rental mobil (tidak ada kapal atau jenis kendaraan lainnya). Mari kita pertimbangkan skema berikut, dengan kendaraan perusahaan dan riwayat layanan pada kendaraan tersebut:

create table vehicle (id SERIAL PRIMARY KEY, plate_no text NOT NULL, maker TEXT NOT NULL, model TEXT NOT NULL,vin text not null);
create table vehicle_service(id SERIAL PRIMARY KEY, vehicleid INT NOT NULL REFERENCES vehicle(id), service TEXT NOT NULL, date_performed DATE NOT NULL DEFAULT now(), cost real not null);
rentaldb=# insert into vehicle (plate_no,maker,model,vin) VALUES ('INI888','Hyundai','i20','HH999');
rentaldb=# insert into vehicle_service (vehicleid,service,cost) VALUES(1,'engine oil change/filters',50);

Sekarang mari kita bayangkan sistem dalam produksi, dan kemudian perusahaan mengakuisisi perusahaan kedua yang melakukan penyewaan kapal dan harus mengintegrasikannya ke dalam sistem, dengan membuat kedua perusahaan beroperasi secara independen sejauh operasi berjalan, tetapi secara terpadu untuk digunakan oleh mgmt atas. Juga, bayangkan bahwa data vehicle_service tidak boleh dipisah karena semua baris harus terlihat oleh kedua perusahaan. Jadi yang kami cari adalah memberikan solusi multi-tenancy berdasarkan pewarisan pada tabel kendaraan. Pertama, kita harus membuat skema baru untuk mobil, (bisnis lama), dan satu untuk kapal, lalu memigrasikan data yang ada ke cars.vehicle:

rentaldb=# create schema cars;
rentaldb=# create table cars.vehicle (CONSTRAINT vehicle_pkey PRIMARY KEY(id) ) INHERITS (public.vehicle);
rentaldb=# \d cars.vehicle
                              Table "cars.vehicle"
  Column  |  Type   | Collation | Nullable |               Default               
----------+---------+-----------+----------+-------------------------------------
 id       | integer |           | not null | nextval('vehicle_id_seq'::regclass)
 plate_no | text    |           | not null |
 maker    | text    |           | not null |
 model    | text    |           | not null |
 vin      | text    |           | not null |
Indexes:
    "vehicle_pkey" PRIMARY KEY, btree (id)
Inherits: vehicle
rentaldb=# create schema boats;
rentaldb=# create table boats.vehicle (CONSTRAINT vehicle_pkey PRIMARY KEY(id) ) INHERITS (public.vehicle);
rentaldb=# \d boats.vehicle
                              Table "boats.vehicle"
  Column  |  Type   | Collation | Nullable |               Default               
----------+---------+-----------+----------+-------------------------------------
 id       | integer |           | not null | nextval('vehicle_id_seq'::regclass)
 plate_no | text    |           | not null |
 maker    | text    |           | not null |
 model    | text    |           | not null |
 vin      | text    |           | not null |
Indexes:
    "vehicle_pkey" PRIMARY KEY, btree (id)
Inherits: vehicle

Kami mencatat bahwa tabel baru berbagi nilai default yang sama untuk kolom id (urutan yang sama) sebagai tabel induk. Meskipun ini jauh dari solusi untuk masalah keunikan global yang dijelaskan di bagian sebelumnya, ini adalah solusi, asalkan tidak ada nilai eksplisit yang akan digunakan untuk sisipan atau pembaruan. Jika semua tabel anak (cars.vehicle dan boats.vehicle) didefinisikan seperti di atas, dan kami tidak pernah secara eksplisit memanipulasi id, maka kami akan aman.

Karena kita hanya akan menyimpan tabel public vehicle_service dan ini akan mereferensikan baris tabel anak, kita harus menghilangkan batasan FK:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Tetapi karena kita perlu mempertahankan konsistensi yang setara dalam database kita, kita harus mencari solusi untuk ini. Kami akan menerapkan batasan ini menggunakan pemicu. Kita perlu menambahkan pemicu ke vehicle_service yang memeriksa bahwa untuk setiap INSERT atau UPDATE, vehicleid menunjuk ke baris yang valid di suatu tempat dalam hierarki public.vehicle*, dan satu pemicu pada setiap tabel hierarki ini yang memeriksanya untuk setiap DELETE atau UPDATE pada id, tidak ada baris di vehicle_service yang menunjuk ke nilai lama. (catatan oleh kendaraan* notasi PostgreSQL menyiratkan ini dan semua tabel anak-anak)

CREATE OR REPLACE FUNCTION public.vehicle_service_fk_to_vehicle() RETURNS TRIGGER
        LANGUAGE plpgsql
AS $$
DECLARE
tmp INTEGER;
BEGIN
        IF (TG_OP = 'DELETE') THEN
          RAISE EXCEPTION 'TRIGGER : % called on unsuported op : %',TG_NAME, TG_OP;
        END IF;
        SELECT vh.id INTO tmp FROM public.vehicle vh WHERE vh.id=NEW.vehicleid;
        IF NOT FOUND THEN
          RAISE EXCEPTION '%''d % (id=%) with NEW.vehicleid (%) does not match any vehicle ',TG_OP, TG_TABLE_NAME, NEW.id, NEW.vehicleid USING ERRCODE = 'foreign_key_violation';
        END IF;
        RETURN NEW;
END
$$
;
CREATE CONSTRAINT TRIGGER vehicle_service_fk_to_vehicle_tg AFTER INSERT OR UPDATE ON public.vehicle_service FROM public.vehicle DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE public.vehicle_service_fk_to_vehicle();

Jika kita mencoba mengupdate atau menyisipkan nilai untuk kolom vehicleid yang tidak ada di vehicle* maka kita akan mendapatkan error:

rentaldb=# insert into vehicle_service (vehicleid,service,cost) VALUES(2,'engine oil change/filters',50);
ERROR:  INSERT'd vehicle_service (id=2) with NEW.vehicleid (2) does not match any vehicle
CONTEXT:  PL/pgSQL function vehicle_service_fk_to_vehicle() line 10 at RAISE

Sekarang jika kita menyisipkan baris di tabel mana pun dalam hierarki mis. boats.vehicle (yang biasanya akan mengambil id=2) dan coba lagi:

rentaldb=# insert into boats.vehicle (maker, model,plate_no,vin) VALUES('Zodiac','xx','INI000','ZZ20011');
rentaldb=# select * from vehicle;
 id | plate_no |  maker  | model |   vin   
----+----------+---------+-------+---------
  1 | INI888   | Hyundai | i20   | HH999
  2 | INI000   | Zodiac  | xx    | ZZ20011
(2 rows)
rentaldb=# insert into vehicle_service (vehicleid,service,cost) VALUES(2,'engine oil change/filters',50);

Kemudian INSERT sebelumnya sekarang berhasil. Sekarang kita juga harus melindungi hubungan FK ini di sisi lain, kita harus memastikan bahwa tidak ada pembaruan/penghapusan yang diizinkan pada tabel mana pun dalam hierarki jika baris yang akan dihapus (atau diperbarui) direferensikan oleh vehicle_service:

CREATE OR REPLACE FUNCTION public.vehicle_fk_from_vehicle_service() RETURNS TRIGGER
        LANGUAGE plpgsql
AS $$
DECLARE
tmp INTEGER;
BEGIN
        IF (TG_OP = 'INSERT') THEN
          RAISE EXCEPTION 'TRIGGER : % called on unsuported op : %',TG_NAME, TG_OP;
        END IF;
        IF (TG_OP = 'DELETE' OR OLD.id <> NEW.id) THEN
          SELECT vhs.id INTO tmp FROM vehicle_service vhs WHERE vhs.vehicleid=OLD.id;
          IF FOUND THEN
            RAISE EXCEPTION '%''d % (OLD id=%) matches existing vehicle_service with id=%',TG_OP, TG_TABLE_NAME, OLD.id,tmp USING ERRCODE = 'foreign_key_violation';
          END IF;
        END IF;
        IF (TG_OP = 'UPDATE') THEN
                RETURN NEW;
        ELSE
                RETURN OLD;
        END IF;
END
$$
;
CREATE CONSTRAINT TRIGGER vehicle_fk_from_vehicle_service AFTER DELETE OR UPDATE
ON public.vehicle FROM vehicle_service DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE vehicle_fk_from_vehicle_service();
CREATE CONSTRAINT TRIGGER vehicle_fk_from_vehicle_service AFTER DELETE OR UPDATE
ON cars.vehicle FROM vehicle_service DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE vehicle_fk_from_vehicle_service();
CREATE CONSTRAINT TRIGGER vehicle_fk_from_vehicle_service AFTER DELETE OR UPDATE
ON boats.vehicle FROM vehicle_service DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE vehicle_fk_from_vehicle_service();

Mari kita coba:

rentaldb=# delete from vehicle where id=2;
ERROR:  DELETE'd vehicle (OLD id=2) matches existing vehicle_service with id=3
CONTEXT:  PL/pgSQL function vehicle_fk_from_vehicle_service() line 11 at RAISE

Sekarang kita perlu memindahkan data yang ada di public.vehicle ke cars.vehicle.

rentaldb=# begin ;
rentaldb=# set constraints ALL deferred ;
rentaldb=# set session_replication_role TO replica;
rentaldb=# insert into cars.vehicle select * from only public.vehicle;
rentaldb=# delete from only public.vehicle;
rentaldb=# commit ;

Menyetel session_replication_role TO replika mencegah pemicu normal. Perhatikan bahwa, setelah memindahkan data, kita mungkin ingin sepenuhnya menonaktifkan tabel induk (public.vehicle) untuk menerima sisipan (kemungkinan besar melalui aturan). Dalam hal ini, dalam analogi OO, kita akan memperlakukan public.vehicle sebagai kelas abstrak, yaitu tanpa baris (instance). Menggunakan desain ini untuk multi-tenancy terasa wajar karena masalah yang akan dipecahkan adalah use case klasik untuk pewarisan, namun masalah yang kami hadapi tidak sepele. Ini telah didiskusikan oleh komunitas peretas, dan kami berharap untuk perbaikan di masa mendatang.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Tantangan Menskalakan Database PostgreSQL Moodle

  2. Hapus beberapa array secara paralel

  3. Jalankan kueri tab silang dinamis

  4. Cara mendapatkan teks SQL dari pemicu acara Postgres

  5. Aplikasi Boot Musim Semi macet di Hikari-Pool-1 - Memulai...