[英]Rails, Postgres: How to CREATE a pivot table and LEFT JOIN it to another table without hardcoding the columns
I am working in Rails and Postgres.我在 Rails 和 Postgres 工作。 I have a table Problems , which has a few columns.
我有一个表Problems ,其中有几列。 I have another table ExtraInfos , which references Problems and has three columns: problem_id, info_type, info_value.
我有另一个表ExtraInfos ,它引用问题并具有三列:problem_id、info_type、info_value。
For example:例如:
Problems:问题:
id ![]() |
problem_type![]() |
problem_group![]() |
---|---|---|
0 ![]() |
type_x![]() |
grp_a ![]() |
1 ![]() |
type_y![]() |
grp_b ![]() |
2 ![]() |
type_z![]() |
grp_c ![]() |
ExtraInfos:额外信息:
id ![]() |
problem_id![]() |
info_type:String![]() |
info_value![]() |
---|---|---|---|
0 ![]() |
0 ![]() |
info_1![]() |
v1 ![]() |
1 ![]() |
0 ![]() |
info_2![]() |
v2 ![]() |
2 ![]() |
0 ![]() |
info_3![]() |
v3 ![]() |
3 ![]() |
1 ![]() |
info_1![]() |
v4 ![]() |
4 ![]() |
1 ![]() |
info_3![]() |
v5 ![]() |
As you can see, each problem has a variable number of extra information.如您所见,每个问题都有数量不定的额外信息。
What is the best way to join both tables to create something that looks like:连接两个表以创建类似以下内容的最佳方法是什么:
id ![]() |
problem_type![]() |
problem_group![]() |
info_1![]() |
info_2![]() |
info_3![]() |
---|---|---|---|---|---|
0 ![]() |
type_x![]() |
grp_a ![]() |
v1 ![]() |
v2 ![]() |
v3 ![]() |
1 ![]() |
type_y![]() |
grp_b ![]() |
v4 ![]() |
v5 ![]() |
|
2 ![]() |
type_z![]() |
grp_c ![]() |
I used the ruby pivot_table gem, and I did manage to create the view that I wanted, by我使用了 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
and then iterating over it by然后迭代它
...
<% @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 %>
...
but this is very clunky and doesn't leave me with good ways to, for instance, sort by these extra columns.但这非常笨重,并没有给我留下好的方法,例如,按这些额外的列进行排序。 As far as I know, the tables are simply a grid, an object, and can't
LEFT JOIN
with my Problems.all
table, which would be the ideal solution.据我所知,这些表只是一个网格、一个对象,并且不能与我的
Problems.all
表进行LEFT JOIN
,这将是理想的解决方案。
I have tried looking up various pure SQL methods, but all seem to start with the assumption that these extra columns will be hard coded in, which is what I am trying to avoid.我曾尝试查找各种纯 SQL 方法,但所有方法似乎都以这样的假设开始:这些额外的列将被硬编码,这是我试图避免的。 I came across crosstab , but I haven't managed to get it working as it should.
我遇到了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)
This gives me the result {"problem_id"=>44, "info_type"=>"6", "info_value"=>"15"} {"problem_id"=>45, "info_type"=>"6", "info_value"=>"15"}
which is clearly not correct.这给了我结果
{"problem_id"=>44, "info_type"=>"6", "info_value"=>"15"} {"problem_id"=>45, "info_type"=>"6", "info_value"=>"15"}
这显然是不正确的。
Another method seems to be creating a separate reference table containing a list of all possible infoTypes, which will then be referenced by the ExtraInfos table, making it easier to join the tables.另一种方法似乎是创建一个单独的引用表,其中包含所有可能的 infoType 的列表,然后将由 ExtraInfos 表引用,从而更容易连接表。 However, I don't want the infoTypes coded in at all.
但是,我根本不想编码 infoTypes。 I want the user to be able to give me any type and value strings, and my tables should be able to deal with this.
我希望用户能够给我任何类型和值字符串,我的表应该能够处理这个问题。
What is the best solution for accomplishing this?实现这一目标的最佳解决方案是什么?
ActiveRecord is built on top of the AST query assembler Arel
. ActiveRecord 建立在 AST 查询汇编器
Arel
之上。
You can use this assembler to build dynamic queries as needed basically if you can hand type it as a SQL query Arel can build it.如果您可以将其手动键入为 Arel 可以构建的 SQL 查询,则基本上可以使用此汇编程序根据需要构建动态查询。
In this case the following will build your desired crosstab query based on the table structure provided in the post.在这种情况下,以下内容将根据帖子中提供的表结构构建您想要的交叉表查询。
# 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)
Using this q.to_sql
will produce:使用这个
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))
And results in并导致
problem_id![]() |
info_1![]() |
info_2![]() |
info_3![]() |
---|---|---|---|
0 ![]() |
v1 ![]() |
v2 ![]() |
v3 ![]() |
1 ![]() |
v4 ![]() |
v5 ![]() |
We can join this to the problems table as我们可以将其加入问题表中
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]}
)
This will return Problem
objects where the info_type
columns are virtual attributes.这将返回
Problem
对象,其中info_type
列是虚拟属性。 eg out.first.info_1 #=> 'v1'
例如
out.first.info_1 #=> 'v1'
Note: Personally I would break the parts down in a class to make the assembly clearer but the above will produce the desired outcome注意:我个人会在一个类中分解零件以使装配更清晰,但以上会产生所需的结果
In postgres, pivot table or crosstab are not relevant when the list of columns may vary in time, ie the list of values in column info_type
may increase or decrease.在 postgres 中,当列列表可能随时间变化时,数据透视表或交叉表不相关,即列
info_type
中的值列表可能增加或减少。
There is an other solution which consists in creating a composite type
dynamically and then using the standard functions jsonb_build_agg
and jsonb_populate_record
:还有另一种解决方案,包括动态创建
composite type
,然后使用标准函数jsonb_build_agg
和jsonb_populate_record
:
Creating the composite type column_list
dynamically:动态创建复合类型
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 ; $$ ;
Then setting up the composite type column_list
for the first time:然后第一次设置复合类型
column_list
:
CALL column_list() ;
But this composite type must be updated after every change of column ExtraInfos.但是这种复合类型必须在每次更改 ExtraInfos 列后更新。 This can be achieved with a trigger function:
这可以通过触发功能来实现:
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 () ;
The final query is:最终查询是:
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
which gives the result:结果是:
id ![]() |
problem_type![]() |
problem_group![]() |
info_1![]() |
info_2![]() |
info_3![]() |
---|---|---|---|---|---|
0 ![]() |
type_x![]() |
grp_a ![]() |
v1 ![]() |
v2 ![]() |
v3 ![]() |
1 ![]() |
type_y![]() |
grp_b ![]() |
v4 ![]() |
null![]() |
v5 ![]() |
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.