sql >> データベース >  >> RDS >> PostgreSQL

ORDBMSの「O」:PostgreSQLの継承

    このブログエントリでは、PostgreSQLの継承について説明します。これは、初期のリリース以降、従来はPostgreSQLの主要な機能の1つでした。 PostgreSQLでの継承の一般的な使用法は次のとおりです。

    • テーブルのパーティション分割
    • マルチテナンシー

    バージョン10までのPostgreSQLは、継承を使用してテーブルのパーティション分割を実装していました。 PostgreSQL 10は、宣言型パーティショニングの新しい方法を提供します。継承を使用したPostgreSQLのパーティショニングはかなり成熟したテクノロジーであり、十分に文書化され、テストされていますが、データモデルの観点からのPostgreSQLの継承は(私の意見では)それほど普及していないため、このブログではより古典的な使用例に焦点を当てます。以前のブログ(PostgreSQLのマルチテナンシーオプション)から、マルチテナンシーを実現する方法の1つは、個別のテーブルを使用し、ビューを介してそれらを統合することであることがわかりました。この設計の欠点もわかりました。このブログでは、継承を使用してこの設計を強化します。

    継承の概要

    セパレートテーブルとビューで実装されたマルチテナンシーメソッドを振り返ると、その主な欠点は、挿入/更新/削除を実行できないことです。 レンタルの更新を試みた瞬間 このエラーが発生することを確認してください:

    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.

    したがって、レンタルでトリガーまたはルールを作成する必要があります。 挿入/更新/削除を処理する関数を指定するビュー。別の方法は、継承を使用することです。前のブログのスキーマを変更しましょう:

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

    次に、メインの親テーブルを作成しましょう:

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

    OOの用語では、このテーブルはスーパークラスに対応します(Javaの用語では)。次に、継承によって子テーブルを定義しましょう。 public.rentalから、ドメインに固有の各テーブルの列を追加します。例:車の場合は必須の運転免許証番号とオプションのボートセーリング証明書。

    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);

    2つのテーブルcars.rental およびboats.rental 親からすべての列を継承します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

    会社を省略したことに気づきました 親テーブルの定義の列(および結果として子テーブルでも)。テナントのIDはテーブルのフルネームに含まれているため、これは不要になりました。クエリでこれを見つける簡単な方法については、後で説明します。次に、3つのテーブルにいくつかの行を挿入しましょう(顧客を借ります) 前のブログのスキーマとデータ):

    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);

    次に、表の内容を見てみましょう。

    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)

    したがって、オブジェクト指向言語(Javaなど)に存在するのと同じ継承の概念がPostgreSQLにも存在します。これは次のように考えることができます。
    public.rental:superclass
    cars.rental:subclass
    boats.rental:subclass
    row public.rental.id =1:instance of public.rental
    row cars.rental.id =2:cars.rentalおよびpublic.rentalのインスタンス
    row boats.rental.id =3:boats.rentalおよびpublic.rentalのインスタンス

    >

    boats.rentalとcars.rentalの行もpublic.rentalのインスタンスであるため、public.rentalの行として表示されるのは当然です。 public.rentalを除く行(つまり、public.rentalに直接挿入された行)のみが必要な場合は、次のようにONLYキーワードを使用して行います。

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

    継承に関するJavaとPostgreSQLの違いの1つは、次のとおりです。Javaは多重継承をサポートしていませんが、PostgreSQLはサポートしています。複数のテーブルから継承できるため、この点で、テーブルはインターフェイスのように考えることができます。 Javaで。

    特定の行が属する階層内の正確なテーブル(Javaのobj.getClass()。getName()に相当)を見つけたい場合は、tableoid特殊列(<のそれぞれのテーブルのoid)を指定することで実行できます。 em> pgclass )、完全なテーブル名を与えるregclassにキャストされます:

    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)

    上記(異なるtableoid)から、階層内のテーブルは、継承関係に接続された単なる古いPostgreSQLテーブルであると推測できます。しかし、これに加えて、それらは通常のテーブルとほとんど同じように機能します。そして、これは次のセクションでさらに強調されます。

    PostgreSQLの継承に関する重要な事実と警告

    子テーブルは次を継承します:

    • NOTNULL制約
    • チェック制約

    子テーブルは継承しません:

    • 主キーの制約
    • 一意の制約
    • 外部キーの制約

    同じ名前の列が階層上の複数のテーブルの定義に表示される場合、それらの列は同じタイプである必要があり、1つの単一の列にマージされます。階層内のどこかに列名にNOTNULL制約が存在する場合、これは子テーブルに継承されます。同じ名前のCHECK制約もマージされ、同じ条件である必要があります。

    親テーブルへのスキーマ変更(ALTER TABLEを介して)は、この親テーブルの下に存在する階層全体に伝播されます。そして、これはPostgreSQLの継承の優れた機能の1つです。

    セキュリティおよびセキュリティポリシー(RLS)は、使用する実際のテーブルに基づいて決定されます。親テーブルを使用する場合、それらのテーブルのセキュリティとRLSが使用されます。親テーブルに対する特権を付与すると、子テーブルにも権限が付与されますが、これは親テーブルを介してアクセスされた場合に限ります。子テーブルに直接アクセスするには、明示的なGRANTを子テーブルに直接与える必要があります。親テーブルに対する権限では不十分です。同じことがRLSにも当てはまります。

    トリガーの起動に関しては、ステートメントレベルのトリガーはステートメントの名前付きテーブルに依存しますが、行レベルのトリガーは実際の行が属するテーブルに応じて起動されます(したがって、子テーブルである可能性があります)。

    注意事項:

    • ほとんどのコマンドは階層全体で機能し、ONLY表記をサポートします。ただし、一部の低レベルコマンド(REINDEX、VACUUMなど)は、コマンドで指定された物理テーブルでのみ機能します。疑わしい場合は、毎回必ずドキュメントを読んでください。
    • FOREIGN KEY制約(参照側にある親テーブル)は継承されません。これは、階層のすべての子テーブルに同じFK制約を指定することで簡単に解決できます。
    • この時点(PostgreSQL 10)の時点では、テーブルのグループにグローバルUNIQUE INDEX(PRIMARY KEYSまたはUNIQUE制約)を設定する方法はありません。この結果:
      • PRIMARY KEYおよびUNIQUE制約は継承されません。また、階層のすべてのメンバーにわたって列に一意性を適用する簡単な方法はありません。
      • 親テーブルがFOREIGNKEY制約の参照側にある場合、チェックは、子テーブルではなく、真に(物理的に)親テーブルに属する行の列の値に対してのみ行われます。
      • >

    最後の制限は深刻なものです。公式ドキュメントによると、これに対する適切な回避策はありません。ただし、FKと一意性は、本格的なデータベース設計の基本です。これに対処する方法を検討します。

    今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードする

    実際の継承

    このセクションでは、プレーンテーブル、PRIMARY KEY / UNIQUE、およびFOREIGN KEY制約を使用するクラシックなデザインを、継承に基づくマルチテナントデザインに変換し、(前のセクションで予想される)問題の解決を試みます。顔。前のブログで例として使用したのと同じレンタルビジネスを考えてみましょう。最初は、ビジネスがレンタカーのみを行っている(ボートや他の種類の車両は使用していない)と想像してみてください。会社の車両とそれらの車両のサービス履歴を含む次のスキーマを考えてみましょう。

    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);

    ここで、システムが稼働していると想像してみてください。次に、ボートのレンタルを行う2番目の会社を買収し、2つの会社を独立して運営することで、ボートのレンタルをシステムに統合する必要があります。トップ管理による使用。また、すべての行が両方の会社に表示される必要があるため、Vehicle_serviceデータを分割してはならないことを想像してみてください。したがって、私たちが探しているのは、車両テーブルの継承に基づくマルチテナンシーソリューションを提供することです。まず、自動車用の新しいスキーマ(古いビジネス)とボート用のスキーマを作成してから、既存のデータを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

    新しいテーブルは、列 idと同じデフォルト値を共有していることに注意してください。 (同じシーケンス)親テーブルと同じ。これは、前のセクションで説明したグローバルな一意性の問題の解決策にはほど遠いですが、挿入または更新に明示的な値が使用されないという条件で、回避策です。すべての子テーブル(cars.vehicleとboats.vehicle)が上記のように定義されていて、IDを明示的に操作しない場合は、安全です。

    public Vehicle_serviceテーブルのみを保持し、これは子テーブルの行を参照するため、FK制約を削除する必要があります:

    rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

    ただし、データベースで同等の整合性を維持する必要があるため、この回避策を見つける必要があります。トリガーを使用してこの制約を実装します。 Vehicle_serviceにトリガーを追加して、INSERTまたはUPDATEごとに、vehicleidがpublic.vehicle *階層のどこかで有効な行を指していることを確認し、この階層の各テーブルに1つのトリガーを追加して、DELETEまたはidのUPDATE、古い値を指すvehicle_serviceの行は存在しません。 (ビークル*表記による注意:PostgreSQLはこれとすべての子テーブルを意味します)

    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();

    Vehicle *に存在しない列Vehicleidの値を更新または挿入しようとすると、エラーが発生します:

    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

    ここで、階層内の任意のテーブルに行を挿入するとします。 boats.vehicle(通常はid =2を取ります)そして再試行します:

    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);

    その後、前のINSERTが成功します。ここで、反対側のこのFK関係も保護する必要があります。削除(または更新)する行が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();

    試してみましょう:

    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

    次に、public.vehicleの既存のデータを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 ;

    session_replication_roleをレプリカに設定すると、通常のトリガーが発生しなくなります。データを移動した後、挿入を受け入れる親テーブル(public.vehicle)を完全に無効にしたい場合があることに注意してください(おそらくルールを介して)。この場合、OOの例えでは、public.vehicleを抽象クラスとして、つまり行(インスタンス)なしで扱います。解決すべき問題は継承の典型的なユースケースであるため、この設計をマルチテナンシーに使用することは自然なことですが、私たちが直面した問題は些細なことではありません。これはハッカーコミュニティによって議論されており、将来の改善を期待しています。


    1. 互換性レベルとカーディナリティ推定入門書

    2. カスタムソートの実装

    3. AWSS3のPostgreSQLバックアップメソッドの機能

    4. PHPPDOプリペアドステートメント-MySQLLIKEクエリ