Ruby on RailsによるPostgresqlテーブルの継承

Postgres継承への移行


それは何で、なぜ必要なのですか?


さまざまな種類の資料がある主要なニュース出版物があるとします。


マテリアルの各タイプには、 Topics::ArticleTopics::OnlineTopics::NewsItemなどの独自のモデルがあります。 タイトル、カバー、テキスト、著者などのフィールドの大部分が同じになります。 違いは、各タイプのトピックに固有のいくつかの特定のフィールドのみです。


したがって、それらを別々のテーブルに配置する必要はありません。 ほとんど完全に繰り返されるテーブルを作成することに抵抗があることに加えて、これにはいくつかの他の理由があります。 これらのタイプ、UNIONウォーターフォール、および接続モデルのポリモーフィズムのさまざまな組み合わせを持つ複雑なサンプルの必要性。


catの下で、Postgresql内で同様のモデルを整理し、その結果、テーブルを継承する移行の形での経験。 銀の弾丸で脚を撃つこともありますが、それがない場合です。


単一テーブルの継承


最初に頭に浮かぶのは、古典的な単一テーブルの継承です。 Railsでは、あるモデルを別のモデルから継承すると自動的にオンになります。


1つの共通トピックテーブルを作成し、それにタイプサービスフィールドを追加することにより、1つのテーブル内にすべてのクラスを格納できます。


 class Topic < ActiveRecord::Base end class Topics::Article < Topic end class Topics::NewsItem < Topic end ... 

一般的なロジック(投稿資料など)はトピックに移動します。 継承されたクラスに固有。


このシンプルで実行中の回路にはスケーリングの問題があります。 15を超える種類のトピックがあり、それぞれに2〜10個の一意のフィールドがある場合はどうすればよいですか。


この時点で、純粋主義者は激しい歯痛のように眉をひそめます-線が常に15-20%以下で満たされているテーブルを想像してください。


STI + Jsonb


STI + Jsonb


ジョンブ


Postgresql c 9.4には、jsonbなどのフィールドを作成する機能があります。 これはどのように役立ちますか?
このタイプのデータフィールドをトピックに追加することにより、すべての追加フィールドをjsonキーに格納できます。


Railsで次のように接続できます。


 class Topics::Online < Topic store_accessor :data, :start_at, :live end 

これで次のことができます。


 online = Topics::Online.new(live: true) 

 online.live # => true 

または、jsonに直接連絡してください:


 online['data']['live'] # => true 

Jsonbの問題


成功の楽しみは、松葉杖の小道具によってすぐに覆い隠されます。


型変換


配列やオブジェクト(ハッシュ)などのエキゾチックに加えて、jsonbはすべてのフィールドにNumber、String、およびBooleanのみを使用することを提案しています。
他のタイプのフィールドの場合、追加のメソッドを記述する必要があります。 そして、砂糖を好むなら、これらの畑にも。


述語:


 def live? live == true end 

DateTimeのより複雑なケース:


 def start_at return Time.zone.parse(super) if super.is_a?(String) end 

ここでは、文字列を時間内に解析する必要があり、中断しないでください。 データを保存する段階で分解する必要があるからです。


入力値の検証


dbレベルには型の検証はありません;そのようなトピックを簡単かつ自然に保存できます:


 online.live = 'Elvis' online.start_at = 'Presley' 

activerecord-typedstoreなどの既存のgemは、文字列の解析の問題を部分的に解決しますが、入力値のチェックにはまったく対処できません。 カスタム検証を使用して、アプリケーションからすべてを閉じる必要があります。


かさばるクエリ


ブールフィールド:


 scope live, -> { where(live: true) } 

 scope live, -> { where("(params->>'live')::bool = ?", true) } 

そして今、日付:


 scope :by_range, ->(start_date, end_date) { where(date: start_date..end_date) } 

 scope :by_range, lambda { |start_date, end_date| where( "to_date(params->>'date', 'YYYY-MM-DD') BETWEEN ? AND ?", start_date, end_date ) } 

一般的な怪物に加えて、 to_date Postgresを強制的に使用するため、このリクエストの動作も遅くなります。


ユニック


Postgresはjsonbを使用したエントリに対して通常のDISTINCT(.uniq)を実行する方法を知りません。これを行う必要があります。


 .select('DISTINCT ON (tags.id) tags.*') 

デフォルトなし


before_initializeでは、通常のnull: false, default: false代わりに、異なる構成を使用する必要があります。


コミュニケーションズ


レールhas_manyを使用すると、 belongs_toは機能しません。 あなた自身の何かを書く必要があります。


この時点で、jsonbは黒いマークを取得し、一andのパンからトロリーに到達しませんでした。


Postgres継承への移行


テーブルの継承は長い間Postgresに登場し(ほとんどの場合、バージョンを更新する必要はありません)、クラス継承の概念は近いです。
クラスだけでなく、テーブルだけでなく、RubyではなくPostgresで。


トピックテーブルはまだありますが、このテーブル自体の追加フィールドセットではなく、各クラスに固有のフィールドのみを含む追加テーブルを使用してテーブルを展開しています。


例を示す最も簡単な方法:


 CREATE TABLE topics ( headline text, author_id int ); CREATE TABLE topics_onlines ( status char(2) ) INHERITS (topics); 

topics_onlinesを作成することで、3つのフィールドすべてを含む通常のテーブルのように作業できます。


 class Topics::Online < Topic # headline, author_id, status end 

シンプルで美しく、コードを大量に書き換える必要はありません。


Postgresの継承+ Rails


 SELECT c.tableoid, c.headline, c.author_id FROM topics c 

  tableoid | headline | author_id ----------+-----------+---------- 139793 | Las Vegas | 2174 139793 | Mariposa | 1953 139798 | Madison | 845 

親テーブルと子テーブルは、tableoidを介してPostgres内でリンクされます。 Explainではtableoid結合は表示されません。すべてPostgres内で機能します。


アプリケーションから、 topics_onlinesは継承のない最も一般的なテーブルのようになり、 topicsすべてのフィールドとtopics_onlinesオンライン固有のフィールドがtopics_onlinesます。


そして、これは、Railの側から、テーブルを作成するマイグレーションを記述するだけでよいことを意味します。
そして、そのテーブル継承はどのフレームワークでも使用できます。


STIからPGIへの移行


このすべての喜びを活用するには、移行を作成する必要があります。


まず、RailsでのSQL移行用の標準ラッパーが必要です。


 class CreateInheritanceTablesForTopics < ActiveRecord::Migration def change reversible do |dir| dir.up do ... end end end 

次に、コードが省略記号に挿入されます。 コードシートをすぐにスローしないように、移行をバッチで示します。


すべてのトピックテーブルで一意性をチェックするトリガー


作成しますが、これまではPostgresqlでトリガーを使用しません。


 CREATE OR REPLACE FUNCTION check_for_topic_dups() RETURNS trigger AS $func$ BEGIN PERFORM 1 FROM topics where NEW.id=id; IF FOUND THEN RAISE unique_violation USING MESSAGE = 'Duplicate ID: ' || NEW.id; RETURN NULL; END IF; RETURN NEW; END; $func$ LANGUAGE plpgsql; 

そのIDを持つトピックが既に存在する場合、トリガーはエラーをスローします。 これは何かがうまくいかない場合の保険です。


最も重要なPGIの制限は、すべての子テーブルにインデックスと親テーブルの制限がないことです。 つまり、この点で、すべてが異なる物理テーブルのように感じられます。


この条件では、さまざまな種類のトピックに対してIDを繰り返すことはできないため、このトリガーが追加されました。 これはオプションであり、保険として必要です。


テーブル作成


 Topic.descendants.each do |topic_type| sql = <<-SQL CREATE TABLE #{topic_type.pgi_table_name} ( CHECK (type='#{topic_type}') ) INHERITS (topics); CREATE RULE redirect_insert_to_#{topic_type.table_name} AS ON INSERT TO topics WHERE (type='#{topic_type}') DO INSTEAD INSERT INTO #{topic_type.table_name} VALUES (NEW.*); CREATE TRIGGER check_uniquiness_#{topic_type.table_name} BEFORE INSERT ON #{topic_type.table_name} FOR EACH ROW EXECUTE PROCEDURE check_for_topic_dups(); SQL execute(sql) add_index topic_type.table_name, :id end 


もちろん、移行は厳密に制限され、すべてのチェックはRails自体に取り込まれます。


継承されたテーブルにネイティブフィールドを追加する


標準の移行を使用して、新しいテーブルにフィールドを追加できます。


 Class PopulateTopicsTablesWithFields < ActiveRecord::Migration def change add_column :topics_onlines, :start_at, :datetime add_column :topics_news, :main, :boolean, null: false, default: false end end 

STIを完全に取り除く準備ができていない場合は、必要なテーブルがトピッククラスに記述されています。


 class Topics::Online < Topic self.table_name = :topics_online end 

スキーマタイプをsqlに変更するだけです。


 # config/application.rb config.active_record.schema_format = :sql 

これで完了です。


速度-PGI vs jsonb


速度-PGI vs jsonb


パフォーマンスを評価するための最後のステップは興味深いでしょう。 これはすべて、開発を容易にするために考案されたものであるため、PGIの速度のテストに費やす時間はあまりありませんが、いくつかの結論を導き出すことができます。


移行後、PGIとjsonbを使用した古いバージョンの2つのバージョンのアプリケーションが作成されました。
各データベースに5_000_000トピック。


すべてのトピックの数


最も合成的な例:


PGI:


 Topics::Topic.count (8591.6ms) SELECT COUNT(*) FROM "topics" => 5316226 

ジョンブ:


 Topics::Topic.count (8580.1ms) SELECT COUNT(*) FROM "topics" => 5316226 

奇妙な数に驚かないでください、ssd上の場所がなくなるまでトピックが作成されました。


1つのタイプのトピックの数


PGI:


 Gazeta::Topics::Sport::Online.count * (219.5ms) SELECT COUNT(*) FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') => 1000000 

ジョンブ:


 Gazeta::Topics::Sport::Online.count * (419.0ms) SELECT COUNT(*) FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') => 1000000 

ブールフィールドによるリクエスト


インデックスは選択性が高いため使用されません。


PGI:


 Gazeta::Topics::Sport::Online.megauho.explain * Gazeta::Topics::Sport::Online Load (1376.2ms) SELECT "topics_sport_onlines".* FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') AND "topics_sport_onlines"."megauho" = $1 [["megauho", "t"]] 

ジョンブ:


 Gazeta::Topics::Sport::Online.megauho.explain * Gazeta::Topics::Sport::Online Load (5819.6ms) SELECT "topics".* FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') AND ((topics.params->>'megauho')::bool = 't') 

違いは大きい。


制限付きのブール値フィールドのリクエスト


少なくともすでに実世界と交差しています。


PGI:


 Gazeta::Topics::Sport::Online.megauho.limit(1000).explain * Gazeta::Topics::Sport::Online Load (9.1ms) SELECT "topics_sport_onlines".* FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') AND "topics_sport_onlines"."megauho" = $1 LIMIT 1000 [["megauho", "t"]] 

ジョンブ:


 Gazeta::Topics::Sport::Online.megauho.limit(1000).explain * Gazeta::Topics::Sport::Online Load (23.7ms) SELECT "topics".* FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') AND ((topics.params->>'megauho')::bool = 't') LIMIT 1000 

違いがあります。




低選択性クエリのPGI


インデックス検索は、100万件のインデックススキャンから123レコードを返します。


PGI:


 Gazeta::Topics::Sport::Online.megauho.megauho_by_date('2015-12-26').explain * Gazeta::Topics::Sport::Online Load (6.0ms) SELECT "topics_sport_onlines".* FROM "topics_sport_onlines" WHERE "topics_sport_onlines"."type" IN ('Gazeta::Topics::Sport::Online') AND "topics_sport_onlines"."megauho" = $1 AND (topics_sport_onlines.date = '2015-12-26') [["megauho", "t"]] QUERY PLAN ---------- * Index Scan using index_type_megauho_date on topics_sport_onlines (cost=0.42..42.12 rows=20 width=682) Index Cond: (((type)::text = 'Gazeta::Topics::Sport::Online'::text) AND (megauho = true) AND ((date)::text = '2015-12-26'::text)) Filter: megauho (3 rows) 

ジョンブ:


 Gazeta::Topics::Sport::Online.megauho.megauho_by_date('2015-12-26').explain * Gazeta::Topics::Sport::Online Load (7.7ms) SELECT "topics".* FROM "topics" WHERE "topics"."type" IN ('Gazeta::Topics::Sport::Online') AND ((topics.params->>'megauho')::bool = 't') AND (topics.params->>'date' = '2015-12-26') QUERY PLAN ---------- * Index Scan using index_type_megauho_date on topics (cost=0.56..217.61 rows=27 width=948) Index Cond: (((type)::text = 'Gazeta::Topics::Sport::Online'::text) AND ((params ->> 'date'::text) = '2015-12-26'::text)) Filter: ((params ->> 'megauho'::text))::boolean (3 rows) 


jsonbを正しく調理する


モデルを完全に拡張するタスクでは、jsonbは非常に有毒です。 もちろん、PGIにも制限がありますが、私たちのタスクについては、移行中に一度に克服されました。


それでも、jsonbは外部ソースからの緩やかに構造化されたデータに役立ちます。 これらのフィールドはstore_accessorで定義する必要さえありません。そのまま保存するだけで、別のBuilderクラスが有用なものを収集します。


私たちにとっては、外部APIから取得したスポーツ放送がそのようなデータになりました。



Source: https://habr.com/ru/post/J282676/


All Articles