问题
I'd like to create a function that returns items
based on their tags
. However, I do not know how to format an array in the IN()
clause. I believe that is why I get no result.
Here is what I got:
CREATE OR REPLACE FUNCTION getItemsByTag(tags text[])
RETURNS TABLE (id bigint, title text, tag text[]) AS $$
BEGIN
IF array_length(tags, 1) > 0
THEN
EXECUTE format('
SELECT d.id, d.title, array_agg(t.title)
FROM items d
INNER JOIN item_tags dt
ON dt.item_id = d.id
INNER JOIN tags t
ON t.id = dt.tag_id
AND t.title IN (%L)
GROUP BY d.id, d.title
', array_to_string(tags, ','));
-- ELSE ...
END IF;
END;
$$ LANGUAGE plpgsql;
Then when I call:
select getItemsByTag('{"gaming", "sport"}');
I get no result even though there are items tagged with "gaming".
Test case
CREATE TABLE items(
id serial primary key,
title text);
CREATE TABLE tags(
id serial primary key,
title text);
CREATE TABLE item_tags(
item_id int references items(id),
tag_id int references tags(id),
primary key(item_id, tag_id));
insert into items (title) values ('my title 1'), ('my title 2');
insert into tags (title) values ('tag1'), ('tag2');
insert into item_tags (item_id, tag_id) values (1,1), (1, 2);
Function:
CREATE OR REPLACE FUNCTION getItemsByTag(tags text[])
RETURNS TABLE (id bigint, title text, tag text[]) AS $$
BEGIN
IF array_length(tags, 1) > 0
THEN
EXECUTE format('
SELECT d.id, d.title, array_agg(t.title)
FROM items d
INNER JOIN item_tags dt
ON dt.item_id = d.id
INNER JOIN tags t
ON t.id = dt.tag_id
AND t.title IN (%L)
GROUP BY d.id, d.title
', array_to_string(tags, ','));
-- ELSE ...
END IF;
END;
$$ LANGUAGE plpgsql;
Call:
select getItemsByTag('{"tag1", "tag2"}');
回答1:
You are not actually returning the result. You would use RETURN QUERY EXECUTE
for that. Example:
- PostgreSQL parameterized Order By / Limit in table function
But you don't need dynamic SQL here to begin with ...
CREATE OR REPLACE FUNCTION get_items_by_tag(VARIADIC tags text[])
RETURNS TABLE (id int, title text, tag text[]) AS
$func$
BEGIN
IF array_length(tags, 1) > 0 THEN
-- NO need for EXECUTE
RETURN QUERY
SELECT d.id, d.title, array_agg(t.title)
FROM items d
JOIN item_tags dt ON dt.item_id = d.id
JOIN tags t ON t.id = dt.tag_id
AND t.title = ANY ($1) -- use ANY construct
GROUP BY d.id; -- PK covers whole table
-- array_to_string(tags, ',') -- no need to convert array with ANY
-- ELSE ...
END IF;
END
$func$ LANGUAGE plpgsql;
Call with actual array:
SELECT * FROM get_items_by_tag(VARIADIC '{tag1,tag2}'::text[]);
Or call with list of items ("dictionary"):
SELECT * FROM get_items_by_tag('tag1', 'tag2');
Major points
Use
RETURN QUERY
to actually return resulting rows.- PostgreSQL function returning multiple result sets
Don't use dynamic SQL unless you need it. (No
EXECUTE
here.)Use an
ANY
construct instead ofIN
. Why?- How to use ANY instead of IN in a WHERE clause with Rails?
I suggest a
VARIADIC
function for convenience. This way you can either pass an array or a list of items at your choosing. See:- Pass multiple values in single parameter
Avoid mixed-case identifiers in Postgres if possible.
- Are PostgreSQL column names case-sensitive?
Not sure why you have IF array_length(tags, 1) > 0 THEN
, but can probably be replaced with IF tags IS NOT NULL THEN
or no IF
at all and follow up with IF NOT FOUND THEN
. More:
- Dynamic SQL (EXECUTE) as condition for IF statement
回答2:
Try to use in place of tags
in format
statement this:'''' || array_to_string(tags, "','") || ''''
the result in the IN clause will be like IN ('gaming','sport')
.
回答3:
It's because I'm not returning anything.
return query EXECUTE format('
SELECT d.id, d.title, array_agg(t.title)
FROM items d
INNER JOIN item_tags dt
ON dt.item_id = d.id
INNER JOIN tags t
ON t.id = dt.tag_id
AND t.title = ANY(%L)
GROUP BY d.id, d.title
', tags) ;
来源:https://stackoverflow.com/questions/44031084/pass-array-of-tags-to-a-plpgsql-function-and-use-it-in-where-condition