2

I am trying to learn how to call stored procedure from springboot data jpa repository and returns a cursor from stored procedure. While I am implementing I am getting error like org.springframework.dao.InvalidDataAccessApiUsageException: You're trying to execute a @Procedure method without a surrounding transaction that keeps the connection open so that the ResultSet can actually be consumed; Make sure the consumer code uses @Transactional or any other way of declaring a (read-only) transaction] with root cause

My Stored Procedure

CREATE OR REPLACE PROCEDURE public.get_user_cursor(OUT user_cursor refcursor)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
    user_cursor refcursor;
BEGIN
  OPEN user_cursor FOR SELECT nuser_id FROM users;
END;
 $BODY$;

Repository Code

@Transactional(readOnly = true)
@Procedure(name = "Users.get_user_cursor")
List<Users> get_user_cursor();

Users.class

@NamedStoredProcedureQuery(
    name = "Users.get_user_cursor",
    procedureName = "get_user_cursor",
    resultClasses = Users.class,
    parameters = {
        @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, name = "user_cursor", type = void.class)
    }
    
)
 @Table(name = "users")
 public class Users implements Serializable {
     // Model Codes
  }

Even I tried both library import org.springframework.transaction.annotation.Transactional; and import jakarta.transaction.Transactional; and for just @transactional and with readOnly=true. While running application is getting starting without error. But when I calls the API end point, error propogating.

Can anyone suggest where I went wrong in implementation and how can I resolve this error please?

3
  • and returns a cursor You are aware that this is pretty unusual? Why do you think that you need this? Commented Sep 2 at 17:03
  • @Mr.DevEng If there's something more you need clarified, or if something about the fix is still problematic, please add it to the question. I'll be glad to revisit this and fix my answer even if the bounty expires. Half of your previous one burned away. Commented Sep 12 at 8:13
  • I suspect this whole thread came from the advice here. While you can get more rows from a cursor-returning procedure, or one that returns an array/composite/json(b), in both threads the commenters and myself all suggested that if you want more rows, you're better off with a set-returning function, not a procedure. Commented Sep 12 at 8:24

1 Answer 1

2

@Transactional has to move up. Right now, you made it only wrap the actual procedure call.

begin transaction;
call public.get_user_cursor(?);
commit;

From SQL standpoint, this is valid but very much pointless: the routine will return a cursor, which then gets immediately closed at the end of transaction. Quoting 41.7.3. Using Cursors:

All portals are implicitly closed at transaction end. Therefore a refcursor value is usable to reference an open cursor only until the end of the transaction.

The error message is trying to tell you that @Transactional needs to apply to whatever is trying to use the output of get_user_cursor(), not just to get_user_cursor() itself.


Another problem is that your procedure isn't opening the cursor you're trying to open and return. The one you declared as an OUT param:

public.get_user_cursor(OUT user_cursor refcursor)

is separate and distinct from the one you later declared as a variable

DECLARE
    user_cursor refcursor;

You can qualify the identifiers to avoid the confusion: the param with the function name, the column with the table name or alias. Since you don't really need the superfluous cursor, you can also remove the declare entirely: demo at db<>fiddle

CREATE OR REPLACE PROCEDURE public.get_user_cursor(OUT user_cursor refcursor)
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
  OPEN get_user_cursor.user_cursor --qualified with procedure name
   FOR SELECT users.nuser_id --qualified with table name or alias
         FROM users; 
END;
 $BODY$;

It's also possible to label PL/pgSQL blocks with <<blockname>> and later use the label to explicitly address variables from the labelled declare var1 text; using blockname.var1, or address params by their position $1, or override default variable_conflict behaviour:

#variable_conflict error
#variable_conflict use_variable
#variable_conflict use_column

In your case, removing declare would be enough.

I'm not entirely sure how's Spring handling the portal returned this way but in raw SQL it's easier to make that param an inout so that the caller can name the cursor. Otherwise, the procedure responds with a <unnamed portal 2>, incrementing the number to keep the name unique.


If you're doing all of this just to get more than one row out of a SQL routine invocation, define a set-returning function instead of a procedure:
demo2 at db<>fiddle

DROP ROUTINE IF EXISTS public.get_user_cursor(refcursor);

CREATE FUNCTION public.get_nuser_ids()RETURNS SETOF users.nuser_id%type
BEGIN ATOMIC
  SELECT users.nuser_id FROM users; 
END;

If you need whole records of users table:

CREATE FUNCTION public.get_users() RETURNS SETOF users
BEGIN ATOMIC
  SELECT * FROM users; 
END;

Only a subset of users' columns:

CREATE FUNCTION public.get_user_id_name()
  RETURNS TABLE (nuser_id users.nuser_id%type,
                 user_name users.user_name%type)
BEGIN ATOMIC
  SELECT nuser_id, user_name FROM users; 
END;
Sign up to request clarification or add additional context in comments.

1 Comment

to add more contect in this answer, because the answer already cover almost all explanation. @Transactional must happen in the public method.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.