简体   繁体   中英

Is it possible to capture the ID of a created row in postgres to use in the same query?

TL;DR, I'm wondering if it's possible in postgres to create one resource, then create another resource that references it, all within the same statement. I'd like to do this in order to reduce round-trip times and simplify my backend logic.

I currently have two resources, foo and bar. Bar is a child resource of foo that references it:

CREATE TABLE foos (
    id serial PRIMARY KEY,
    name VARCHAR ( 50 ) UNIQUE NOT NULL
);

CREATE TABLE bars (
    id serial PRIMARY KEY,
    name VARCHAR ( 50 ) NOT NULL,
    description VARCHAR ( 50 ),
    foo_id int NOT NULL,
    FOREIGN KEY (foo_id)
      REFERENCES foos (id),
    UNIQUE (foo_id, name) 
);

I currently have the following sql statement to upsert a foo and bar at an endpoint v1/foos/{foo_name}/bars/{bar_name} :

WITH upsert_foo AS
    (
        INSERT INTO "foos" 
        ("name")
        VALUES 
        ($1)
        ON CONFLICT ("name") DO NOTHING
    ),
     upsert_bar AS
      (
         INSERT INTO "bars" 
         ("name", "description", "foo_id" )
         VALUES 
         ($2, $3,   
            (SELECT "id" FROM "foos" WHERE "name" = $1) )
        ON CONFLICT ("name", "foo_id") DO 
        UPDATE SET
                 "description" = EXCLUDED."description",
            RETURNING *
    )
    SELECT upsert_bar.*;

When a user upserts a bar, a foo with the name from the URI should be created if it doesn't already exist, then the bar should be created if it doesn't exist. Otherwise, the bar should be updated.

However, this statement only works in the update case, when the foo resource already exists. It seems that the new foo isn't showing up in (SELECT "id" FROM "foos" WHERE "name" = $1) when it's newly created.

Is there a way that I can capture the ID of the foo that was created (or already exists) and use it when I create the bar object?

The only I can think to do is is break it down into separate operations:

INSERT INTO 
   "foos" ("name")
VALUES 
    ($1)
ON CONFLICT ("name") DO UPDATE 
    SET name = foos.name
RETURNING id;

The above has a throw away UPDATE for purpose of capturing id value. Save this value to variable for use as $4 below.

Then:

INSERT INTO 
   "bars" ("name", "description", "foo_id" )
VALUES 
   ($2, $3, $4)
ON CONFLICT ("name", "foo_id") DO UPDATE 
    SET "description" = EXCLUDED."description"
RETURNING *

I managed to get this working in a single statement, but it was a bit complicated:

WITH insert_foo AS
(
    INSERT INTO "foos" 
    ("name")
    VALUES 
    ($1)
    ON CONFLICT ("name") DO NOTHING RETURNING *
),
upsert_foo AS
(
    SELECT * FROM insert_foo
    UNION
    SELECT * FROM foos WHERE "name"=$1
),
upsert_bar AS
  (
     INSERT INTO "bars" 
     ("name", "description", "foo_id" )
     VALUES 
     ($2, $3, (SELECT "id" FROM upsert_foo WHERE name=$1) )
    ON CONFLICT ("name", "foo_id") DO 
    UPDATE SET
             "description" = EXCLUDED."description"
    RETURNING *
)
SELECT * FROM upsert_bar;

Basically, the result from the insert should be unioned with a regular select query, with only one of them containing a row, depending on whether the foo already existed or not. I can then select the id from the result of this union query.

I want to make a huge shout out to Adrian Klaver for pointing me in the right direction and also this answer: https://stackoverflow.com/a/62205017/4600258

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM