How to deal with mutually recursive inserts

前端 未结 3 972
感情败类
感情败类 2020-12-16 05:35

I have a model that defines mutually recursive tables:

Answer
  questionId QuestionId
  text

Question
  text
  correct AnswerId

What do I

相关标签:
3条回答
  • 2020-12-16 05:51

    I went looking around after seeing the DDL. Consider a function for your call to insert a question with correct answer, and one to add (false) answers to a given question. The structure of the first function allows the application to pick up the anonymous returned record for the questionID, and use it for subsequent calls to the second function, to add false answers.

    CREATE FUNCTION newQuestion (questionText varchar, questionSolutionText varchar, answerText varchar, OUT questionID integer) AS $$
      BEGIN
        START TRANSACTION;
        SET CONSTRAINTS question_correct_fkey DEFERRED;
        questionID := nextval('question_id_seq');
        answerID := nextval('answer_id_seq');
        INSERT INTO question (id, question, correct, solution) values (questionID, questionText, answerID, questionSolutionText);
        INSERT INTO answer (id, text, question_id) values (answerID, answerText, questionID);
        SET CONSTRAINTS question_correct_fkey IMMEDIATE;
        COMMIT TRANSACTION;
      END;
    $$
    CREATE FUNCTION addFalseAnswer (questionID integer, answerText varchar) AS $$
      BEGIN
        INSERT INTO answer (text, question_id) VALUES (answerText, questionID);
      END;
    $$
    

    I've not written SQL for PostGreSQL in a long while, so I hope all is in order here. please let me know if there are any issues.

    0 讨论(0)
  • 2020-12-16 06:01

    If you enter question and answer in a single statement with a data-modifying CTE, you do not even need a DEFERRABLE FK constraints. Not to speak of actually making (or SETting) them DEFERRED - which would be a lot more expensive.

    Data model

    First I cleaned up your data model:

    CREATE TABLE question (
       question_id       serial PRIMARY KEY
     , correct_answer_id int  NOT NULL
     , question          text NOT NULL
     , solution          text NOT NULL
    );
    
    CREATE TABLE answer (
       answer_id   serial PRIMARY KEY
     , question_id int  NOT NULL REFERENCES question
     , answer      text NOT NULL
    );
    
    ALTER TABLE question ADD CONSTRAINT question_correct_answer_id_fkey
    FOREIGN KEY (correct_answer_id) REFERENCES answer(answer_id);
    
    • Don't use the non-descriptive "id" as column name.
    • Don't use basic type names like text as column name.
    • Put the integer columns first for space efficiency:
      • Calculating and saving space in PostgreSQL
    • bigint was uncalled for, integer should suffice.
    • Simplify your schema definition with serial columns.
    • Define primary keys. PK columns are NOT NULL automatically.

    Solution

    I delegated primary key generation to sequences (serial columns) like it should be in most data models. We can get the auto-generated ID with the RETURNING clause of the INSERT statement. But in this special case we need both IDs for each INSERT, so I fetch one of them with nextval() to get the thing started.

    WITH q AS (
       INSERT INTO question (correct_answer_id, question, solution)
       VALUES (nextval('answer_answer_id_seq'), 'How?', 'DEFERRABLE FK & wCTE')
       RETURNING correct_answer_id, question_id
       )
    INSERT INTO answer (answer_id, question_id, answer)
    SELECT correct_answer_id, question_id, 'Use DEFERRABLE FK & data-modifying CTE'
    FROM   q;
    

    I know the name of the sequence ('answer_answer_id_seq') because I looked it up. It's the default name. If you don't know it use the safe form @IMSoP provided in the comment below:

    nextval(pg_get_serial_sequence('answer', 'answer_id'))
    

    DEFERRABLE or DEFERRED constraints?

    Per documentation on SET CONSTRAINTS

    IMMEDIATE constraints are checked at the end of each statement.

    My solution is a single statement. That's why it works where two separate statements would fail - wrapped in a single transaction or not. And you'd need SET CONSTRAINTS ... DEFERRED; like IMSoP first commented and @Jaaz implemented in his answer.
    However, note the disclaimer some paragraphs down:

    Uniqueness and exclusion constraints that have not been declared DEFERRABLE are also checked immediately.

    So UNIQUE and EXCLUDE need to be DEFERRALBE to make wCTEs work for them. This includes PRIMARY KEY constraints. The documentation on CREATE TABLE has more details:

    Non-deferred Uniqueness Constraints

    When a UNIQUE or PRIMARY KEY constraint is not deferrable, PostgreSQL checks for uniqueness immediately whenever a row is inserted or modified. The SQL standard says that uniqueness should be enforced only at the end of the statement; this makes a difference when, for example, a single command updates multiple key values. To obtain standard-compliant behavior, declare the constraint as DEFERRABLE but not deferred (i.e., INITIALLY IMMEDIATE). Be aware that this can be significantly slower than immediate uniqueness checking.

    We discussed this in great detail under this related question:

    • Constraint defined DEFERRABLE INITIALLY IMMEDIATE is still DEFERRED?
    0 讨论(0)
  • 2020-12-16 06:08

    I would insert into question, with a null correct AnswerId. Then I would insert into Answer, and finally I would update Question and set the correct answerId.

    0 讨论(0)
提交回复
热议问题