VF=>user  ID=>    Login Register
Feedback   FAQ   Blog
681,390 quizzes played * 1,624 active players * US$57,334 in prizes awarded

Commentary

Actions


  On Benefits of Dynamic SQL: Remove Repetition with Dynamic SQL (Oracle PL/SQL)      Add to Favorites

Summary

One of the most enjoyable aspects of programming is identifying repetitive code and then getting rid of that redundancy through dynamic SQL. Less code, more code reuse.

Details

Assumes Oracle Database 10g Release 2 or higher
Intermediate Quiz, IDs: 6761/87847/32760

Reviewer(s): Ken Holmslykke, Vitaliy Lyanchevskiy

The Question

It's September 2012 and the PL/SQL Challenge development team is implementing Favorites for questions and topics. We've created tables for each of the different types of favorites:

CREATE TABLE plch_fav_questions
(
   fav_question_id   INTEGER,
   user_id           INTEGER,
   question_id       INTEGER
)
/

CREATE TABLE plch_fav_topics
(
   fav_topic_id   INTEGER,
   user_id        INTEGER,
   topic_id       INTEGER
)
/

The first column in each table is the primary key (its value is assigned in a BEFORE INSERT trigger). The second column is the user who is "liking" the question or topic. Third column is the "favorite ID" - the foreign key back to a row in the table of questions or topics.

I've created a single package to manage all the favorites, with a single procedure that correctly adds a favorite to either of those tables. Here's the specification of the package:

CREATE OR REPLACE PACKAGE plch_favorites_mgr
IS
   /* Favorites Types */

   c_fav_question   CONSTANT VARCHAR2 (100) := 'question';
   c_fav_topic      CONSTANT VARCHAR2 (100) := 'topic';

   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2);
END;
/

But now we've decided to also keep track of favorite Roundtable discussions, so I need to create a new table like this:

CREATE TABLE plch_fav_discussions
(
   fav_discussion_id   INTEGER,
   user_id             INTEGER,
   discussion_id       INTEGER
)
/

Which of the choices implement the body of plch_favorites_mgr, so that after I create plch_fav_discussions, I will be able to call plch_favorites_mgr.add_favorite to add a row to that table, without making any changes to the procedure?

The Choices [↑]

Explanation of Result Icons
Choice 1 (12262)
CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      CASE favorite_type_in
         WHEN c_fav_question
         THEN
            INSERT INTO plch_fav_questions (user_id, question_id)
                 VALUES (user_id_in, favorite_id_in);
         WHEN c_fav_topic
         THEN
            INSERT INTO plch_fav_topics (user_id, topic_id)
                 VALUES (user_id_in, favorite_id_in);
      END CASE;
   END add_favorite;
END;
/

This implementation works fine for the two existing tables, but if I add another favorites table and try to insert into it, I will see this error:

ORA-06592: CASE not found while executing CASE statement

since the CASE statement will not contain a clause for that new table until the body is updated.




75%
Choice 2 (12263)
CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'INSERT INTO '
         || CASE favorite_type_in
               WHEN c_fav_question THEN 'plch_fav_questions'
               WHEN c_fav_topic THEN 'plch_fav_topics'
            END
         || ' (user_id, '
         || CASE favorite_type_in
               WHEN c_fav_question THEN 'question_id'
               WHEN c_fav_topic THEN 'topic_id'
            END
         || ') VALUES (:user_id, :favorite_id)'
         USING user_id_in, favorite_id_in;
   END add_favorite;
END;
/

This implementation works fine for the two existing tables, and it uses dynamic SQL so that there is just one insert statement - less repetition. If, however, I add another favorites table and try to insert into it, I will see this error:

ORA-00928: missing SELECT keyword

since the CASE statement will not contain a clause for that new table until the body is updated, and the resulting insert statement has a values clause that looks like:

(user_id_in,)



78%
Choice 3 (12264) [Recommended Solution]
CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'INSERT INTO plch_fav_'
         || favorite_type_in
         || 's (user_id, '
         || favorite_type_in
         || '_id) VALUES (:user_id, :favorite_id)'
         USING user_id_in, favorite_id_in;
   END add_favorite;
END;
/

I've gotten rid of the repetitive inserts and the hard-codings of he CASE statement by moving to a dynamic SQL implementation that reflects the naming standards in my tables.




80%
Choice 4 (12265) [Do Not Use]
CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'INSERT INTO plch_fav_'
         || favorite_type_in
         || 's (user_id, '
         || favorite_type_in
         || '_id) VALUES ('
         || user_id_in
         || ','
         || favorite_id_in
         || ')';
   END add_favorite;
END;
/

This choice is correct for all the reasons explained in the previous choice.

The difference in this implementation is that I concatenate the user ID and favorite ID, rather than binding the values in the USING clause.

You should bind, rather than concatenate, whenever possible. The resulting code is simpler and performance could be better, since with binding, the SQL statement does not have to be re-parsed with each new set of values.




69%

Answer [↑]

Dynamic SQL is a programming methodology for generating and running SQL statements at run time. It is useful when writing general-purpose and flexible programs like ad hoc query systems, when writing programs that must run database definition language (DDL) statements, or when you do not know at compilation time the full text of a SQL statement or the number or data types of its input and output variables.

PL/SQL provides two ways to write dynamic SQL:

  • Native dynamic SQL, a PL/SQL language (that is, native) feature for building and running dynamic SQL statements

  • DBMS_SQL package, an API for building, running, and describing dynamic SQL statements

Native dynamic SQL code is easier to read and write than equivalent code that uses the DBMS_SQL package, and runs noticeably faster (especially when it can be optimized by the compiler). However, to write native dynamic SQL code, you must know at compile time the number and data types of the input and output variables of the dynamic SQL statement. If you do not know this information at compile time, you must use the DBMS_SQL package.


Repetition in code is another form of hard-coding and something to be avoided whenever possible.

Repetition in code implies a pattern and there are generally two ways to resolve the pattern:

1. Write a reusable subprogram whose parameters accept all the variations in the pattern, and then implement the generalized pattern inside the subprogram.

2. Use dynamic SQL,  with EXECUTE IMMEDIATE and not DBMS_SQL if at all possible, to express the pattern as a statement constructed, and then bind values into the statement at runtime.



Statistics and Rankings [↑]

Rank Percentile Score Seconds% Correct
Median
239
50
319
167
75
Best
1
100
484
49
100

Verify Quiz [↑]

Use the following code to verify the scoring of all the choices in this quiz. If you feel that there is a bug in our code or you disagree with the results, submit an objection in the Commentary section.

CREATE TABLE plch_fav_questions
(
   fav_question_id   INTEGER,
   user_id           INTEGER,
   question_id       INTEGER
)
/

CREATE TABLE plch_fav_topics
(
   fav_topic_id   INTEGER,
   user_id        INTEGER,
   topic_id       INTEGER
)
/

CREATE OR REPLACE PACKAGE plch_favorites_mgr
IS
   /* Favorites Types */

   c_fav_question   CONSTANT VARCHAR2 (100) := 'question';
   c_fav_topic      CONSTANT VARCHAR2 (100) := 'topic';

   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2);
END;
/

/* Full blown repetition with CASE */

CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      CASE favorite_type_in
         WHEN c_fav_question
         THEN
            INSERT INTO plch_fav_questions (user_id, question_id)
                 VALUES (user_id_in, favorite_id_in);
         WHEN c_fav_topic
         THEN
            INSERT INTO plch_fav_topics (user_id, topic_id)
                 VALUES (user_id_in, favorite_id_in);
      END CASE;
   END add_favorite;
END;
/

/* Now create another table and try to add a favorite */

CREATE TABLE plch_fav_discussions
(
   fav_discussion_id   INTEGER,
   user_id             INTEGER,
   discussion_id       INTEGER
)
/

/* ORA-06592: CASE not found while executing CASE statement */

BEGIN
   plch_favorites_mgr.add_favorite (100, 100, 'discussion');
END;
/

/* Shift to Dyn SQL but still constrained by hard codings
   in CASE statement

   ORA-00928: missing SELECT keyword
*/

CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'INSERT INTO '
         || CASE favorite_type_in
               WHEN c_fav_question THEN 'plch_fav_questions'
               WHEN c_fav_topic THEN 'plch_fav_topics'
            END
         || ' (user_id, '
         || CASE favorite_type_in
               WHEN c_fav_question THEN 'question_id'
               WHEN c_fav_topic THEN 'topic_id'
            END
         || ') VALUES (:user_id, :favorite_id)'
         USING user_id_in, favorite_id_in;
   END add_favorite;
END;
/

BEGIN
   plch_favorites_mgr.add_favorite (100, 100, 'discussion');
END;
/

CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'INSERT INTO plch_fav_'
         || favorite_type_in
         || 's (user_id, '
         || favorite_type_in
         || '_id) VALUES (:user_id, :favorite_id)'
         USING user_id_in, favorite_id_in;
   END add_favorite;
END;
/

BEGIN
   plch_favorites_mgr.add_favorite (100, 100, 'discussion');
END;
/

/* Concatenation instead of binding, OK */

CREATE OR REPLACE PACKAGE BODY plch_favorites_mgr
IS
   PROCEDURE add_favorite (user_id_in         IN INTEGER,
                           favorite_id_in     IN INTEGER,
                           favorite_type_in   IN VARCHAR2)
   IS
   BEGIN
      EXECUTE IMMEDIATE
            'INSERT INTO plch_fav_'
         || favorite_type_in
         || 's (user_id, '
         || favorite_type_in
         || '_id) VALUES ('
         || user_id_in
         || ','
         || favorite_id_in
         || ')';
   END add_favorite;
END;
/

BEGIN
   plch_favorites_mgr.add_favorite (100, 100, 'discussion');
END;
/

/* Should be two rows in table (a row per a correct choice) */

SELECT * FROM plch_fav_discussions
/

/* Clean up */

DROP TABLE plch_fav_topics
/

DROP TABLE plch_fav_questions
/

DROP TABLE plch_fav_discussions
/

DROP PACKAGE plch_favorites_mgr
/

Link to Quiz [↑]

Commentary [↑]

Ask for Help     Raise Objection     Start New Discussion     Your Comments
Last: 253 days ago
Replies: 4
Likes: 4
SUBMITTED
Unread Comments
Likes: 3
SUBMITTED
Unread Comments
Last: 254 days ago
Replies: 1
Likes: 0
SUBMITTED
Unread Comments
Last: 249 days ago
Replies: 6
Likes: 0
SUBMITTED
Unread Comments
1 - 4