それは何で、なぜ必要なのですか?
さまざまな種類の資料がある主要なニュース出版物があるとします。
マテリアルの各タイプには、 Topics::Article
、 Topics::Online
、 Topics::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
ジョンブ
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
- トピックの種類ごとにテーブルが作成されます(ここでは、コンパクト化のためにメタデータがより重要です。移行では、これを行わない方が良いでしょう)
- 子テーブルごとに、トピックの種類に対する制限が追加されます(
Topics::Online
のみをtopics_onlines
に挿入できます) - 塗りつぶされたタイプのトピックをトピックに挿入しようとすると、タイプごとに子テーブルにリダイレクトされます
- 各テーブルに、一意の識別子をチェックするための以前に作成されたトリガーを掛けます
- 子テーブルのインデックスを作成する
もちろん、移行は厳密に制限され、すべてのチェックは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の速度のテストに費やす時間はあまりありませんが、いくつかの結論を導き出すことができます。
移行後、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)
- PGIは高速です。
- サンプルが大きいほど高速になります。
- インデックスを使用するクエリの場合、PGIは依然として高速ですが、違いはすでに目立ちません。
jsonbを正しく調理する
モデルを完全に拡張するタスクでは、jsonbは非常に有毒です。 もちろん、PGIにも制限がありますが、私たちのタスクについては、移行中に一度に克服されました。
それでも、jsonbは外部ソースからの緩やかに構造化されたデータに役立ちます。 これらのフィールドはstore_accessor
で定義する必要さえありません。そのまま保存するだけで、別のBuilderクラスが有用なものを収集します。
私たちにとっては、外部APIから取得したスポーツ放送がそのようなデータになりました。