[英]Rails, Postgres: How to CREATE a pivot table and LEFT JOIN it to another table without hardcoding the columns
我在 Rails 和 Postgres 工作。 我有一個表Problems ,其中有幾列。 我有另一個表ExtraInfos ,它引用問題並具有三列:problem_id、info_type、info_value。
例如:
問題:
ID | 問題類型 | 問題組 |
---|---|---|
0 | 類型_x | grp_a |
1個 | 類型_y | grp_b |
2個 | 類型_z | grp_c |
額外信息:
ID | 問題編號 | 信息類型:字符串 | 信息值 |
---|---|---|---|
0 | 0 | 信息_1 | v1 |
1個 | 0 | 信息_2 | v2 |
2個 | 0 | 信息_3 | v3 |
3個 | 1個 | 信息_1 | v4 |
4個 | 1個 | 信息_3 | v5 |
如您所見,每個問題都有數量不定的額外信息。
連接兩個表以創建類似以下內容的最佳方法是什么:
ID | 問題類型 | 問題組 | 信息_1 | 信息_2 | 信息_3 |
---|---|---|---|---|---|
0 | 類型_x | grp_a | v1 | v2 | v3 |
1個 | 類型_y | grp_b | v4 | v5 | |
2個 | 類型_z | grp_c |
我使用了 ruby pivot_table gem,我確實設法創建了我想要的視圖,通過
@table = PivotTable::Grid.new do |g|
g.source_data = ExtraInfos.all.includes(:problem))
g.column_name = :info_type
g.row_name = :problem
g.field_name = :info_value
end
@table.build
然后迭代它
...
<% @table.columns.each do |col| %>
<th><%= col.header %></th>
<% end %>
...
<% if @table.row_headers.include? problem %>
<% table.rows[table.row_headers.index(problem)].data.each do |cell| %>
<td><%= cell %></td>
<% end %>
<% end %>
...
但這非常笨重,並沒有給我留下好的方法,例如,按這些額外的列進行排序。 據我所知,這些表只是一個網格、一個對象,並且不能與我的Problems.all
表進行LEFT JOIN
,這將是理想的解決方案。
我曾嘗試查找各種純 SQL 方法,但所有方法似乎都以這樣的假設開始:這些額外的列將被硬編碼,這是我試圖避免的。 我遇到了crosstab ,但我還沒有設法讓它正常工作。
sql = "CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT * FROM crosstab(
'SELECT problem_id, info_type, info_value
FROM pre_maslas
ORDER BY 1,2'
) AS ct(problem_id bigint, info_type varchar(255), info_value varchar(255))"
@try = ActiveRecord::Base.connection.execute(sql)
這給了我結果{"problem_id"=>44, "info_type"=>"6", "info_value"=>"15"} {"problem_id"=>45, "info_type"=>"6", "info_value"=>"15"}
這顯然是不正確的。
另一種方法似乎是創建一個單獨的引用表,其中包含所有可能的 infoType 的列表,然后將由 ExtraInfos 表引用,從而更容易連接表。 但是,我根本不想編碼 infoTypes。 我希望用戶能夠給我任何類型和值字符串,我的表應該能夠處理這個問題。
實現這一目標的最佳解決方案是什么?
ActiveRecord 建立在 AST 查詢匯編器Arel
之上。
如果您可以將其手動鍵入為 Arel 可以構建的 SQL 查詢,則基本上可以使用此匯編程序根據需要構建動態查詢。
在這種情況下,以下內容將根據帖子中提供的表結構構建您想要的交叉表查詢。
# Get all distinct info_types to build columns
cols = ExtraInfo.distinct.pluck(:info_type).sort
# extra_info Arel::Table
extra_infos_tbl = ExtraInfo.arel_table
# Arel::Table to use for querying
tbl = Arel::Table.new('ct')
# SQL data type for the extra_infos.info_type column
info_type_sql_type = ExtraInfo.columns.find {|c| c.name == 'info_type' }&.sql_type
# Part 1 of crosstab
qry_txt = extra_infos_tbl.project(
extra_infos_tbl[:problem_id],
extra_infos_tbl[:info_type],
extra_infos_tbl[:info_value]
)
# Part 2 of the crosstab
cats = extra_infos_tbl.project(extra_infos_tbl[:info_type]).distinct
# construct the ct portion of the crosstab query
ct = Arel::Nodes::NamedFunction.new('ct',[
Arel::Nodes::TableAlias.new(Arel.sql('problem_id'), Arel.sql('bigint')),
*cols.map {|name| Arel::Nodes::TableAlias.new(Arel.sql(name), Arel.sql(info_type_sql_type))}
])
# build the crosstab(...) AS ct(...) statement
crosstab = Arel::Nodes::As.new(
Arel::Nodes::NamedFunction.new('crosstab', [Arel.sql("'#{qry_txt.to_sql}'"),
Arel.sql("'#{cats.to_sql}'")]),
ct
)
# final query construction
q = tbl.project(tbl[Arel.star]).from(crosstab)
使用這個q.to_sql
將產生:
SELECT
ct.*
FROM
crosstab('SELECT
extra_infos.problem_id,
extra_infos.info_type,
extra_infos.info_value
FROM
extra_infos',
'SELECT DISTINCT
extra_infos.info_type
FROM
extra_infos') AS ct(problem_id bigint,
info_1 varchar(255),
info_2 varchar(255),
info_3 varchar(255))
並導致
問題編號 | 信息_1 | 信息_2 | 信息_3 |
---|---|---|---|
0 | v1 | v2 | v3 |
1個 | v4 | v5 |
我們可以將其加入問題表中
sub = Arel::Table.new('subq')
sub_q = Arel::Nodes::As.new(q,Arel.sql(sub.name))
out = Problem
.joins(Arel::Nodes::InnerJoin.new(sub_q,
Arel::Nodes::On.new(Problem.arel_table[:id].eq(sub[:problem_id]))
)).select(
Problem.arel_table[Arel.star],
*cols.map {|c| sub[c.intern]}
)
這將返回Problem
對象,其中info_type
列是虛擬屬性。 例如out.first.info_1 #=> 'v1'
注意:我個人會在一個類中分解零件以使裝配更清晰,但以上會產生所需的結果
在 postgres 中,當列列表可能隨時間變化時,數據透視表或交叉表不相關,即列info_type
中的值列表可能增加或減少。
還有另一種解決方案,包括動態創建composite type
,然后使用標准函數jsonb_build_agg
和jsonb_populate_record
:
動態創建復合類型column_list
:
CREATE OR REPLACE PROCEDURE column_list() LANGUAGE plpgsql AS $$
DECLARE
clist text ;
BEGIN
SELECT string_agg(DISTINCT info_type || ' text', ',')
INTO clist
FROM ExtraInfos ;
EXECUTE 'DROP TYPE IF EXISTS column_list' ;
EXECUTE 'CREATE TYPE column_list AS (' || clist || ')' ;
END ; $$ ;
然后第一次設置復合類型column_list
:
CALL column_list() ;
但是這種復合類型必須在每次更改 ExtraInfos 列后更新。 這可以通過觸發功能來實現:
CREATE OR REPLACE FUNCTION After_Insert_Update_Delete_ExtraInfos () RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
CALL column_list() ;
RETURN NULL ;
END ; $$ ;
CREATE OR REPLACE TRIGGER After_Insert_Update_Delete_ExtraInfos AFTER INSERT OR UPDATE OF info_type OR DELETE ON ExtraInfos
FOR EACH STATEMENT EXECUTE FUNCTION After_Insert_Update_Delete_ExtraInfos () ;
最終查詢是:
SELECT p.id, p. problem_type, p.problem_group, (jsonb_populate_record(NULL :: column_list, jsonb_object_agg(info_type, info_value))).*
FROM Problems AS p
INNER JOIN ExtraInfos AS ei
ON ei.problem_id = p.id
GROUP BY p.id, p. problem_type, p.problem_group
結果是:
ID | 問題類型 | 問題組 | 信息_1 | 信息_2 | 信息_3 |
---|---|---|---|---|---|
0 | 類型_x | grp_a | v1 | v2 | v3 |
1個 | 類型_y | grp_b | v4 | 無效的 | v5 |
在dbfiddle中查看測試結果
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.