Хранимая процедура MySQL, обработка нескольких курсоров и результатов запроса

Как я могу использовать два курсора в одной и той же процедуре? Если я удалю второе объявление курсора и получу цикл everthing, все работает нормально. Процедура используется для добавления друга в моем веб-приложение. Он принимает идентификатор текущего пользователя и адрес электронной почты друга, которого мы хотим добавить в качестве друга, затем проверяет, имеет ли электронное письмо соответствующий идентификатор пользователя, и если нет дружеских отношений, он создаст его. Любое другое рутинное решение, кроме этого, было бы здорово.

DROP PROCEDURE IF EXISTS addNewFriend;
DELIMITER //
CREATE PROCEDURE addNewFriend(IN inUserId INT UNSIGNED, IN inFriendEmail VARCHAR(80))
BEGIN
    DECLARE tempFriendId INT UNSIGNED DEFAULT 0;
    DECLARE tempId INT UNSIGNED DEFAULT 0;
    DECLARE done INT DEFAULT 0;

    DECLARE cur CURSOR FOR
        SELECT id FROM users WHERE email = inFriendEmail;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    OPEN cur;
    REPEAT
        FETCH cur INTO tempFriendId;
    UNTIL done  = 1 END REPEAT;
    CLOSE cur;

    DECLARE cur CURSOR FOR 
        SELECT user_id FROM users_friends WHERE user_id = tempFriendId OR friend_id = tempFriendId;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

    OPEN cur;
    REPEAT
        FETCH cur INTO tempId;
    UNTIL done  = 1 END REPEAT;
    CLOSE cur;

    IF tempFriendId != 0 AND tempId != 0 THEN
        INSERT INTO users_friends (user_id, friend_id) VALUES(inUserId, tempFriendId);
    END IF;
    SELECT tempFriendId as friendId;
END //
DELIMITER ;

5 ответов


вот простой пример того, как использовать два курсора в одной и той же процедуре:

DELIMITER $$

CREATE PROCEDURE `books_routine`()
BEGIN
  DECLARE rowCountDescription INT DEFAULT 0;
  DECLARE rowCountTitle INT DEFAULT 0;
  DECLARE updateDescription CURSOR FOR
    SELECT id FROM books WHERE description IS NULL OR CHAR_LENGTH(description) < 10;
  DECLARE updateTitle CURSOR FOR
    SELECT id FROM books WHERE title IS NULL OR CHAR_LENGTH(title) <= 10;

  OPEN updateDescription;
  BEGIN
      DECLARE exit_flag INT DEFAULT 0;
      DECLARE book_id INT(10);
      DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET exit_flag = 1;

      updateDescriptionLoop: LOOP
        FETCH updateDescription INTO book_id;
            IF exit_flag THEN LEAVE updateDescriptionLoop; 
            END IF;
            UPDATE books SET description = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' WHERE books.id = book_id;
        SET rowCountDescription = rowCountDescription + 1;
      END LOOP;
  END;
  CLOSE updateDescription;

  OPEN updateTitle;
  BEGIN
      DECLARE exit_flag INT DEFAULT 0;
      DECLARE book_id INT(10);
      DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET exit_flag = 1;

      updateTitleLoop: LOOP
        FETCH updateTitle INTO book_id;
            IF exit_flag THEN LEAVE updateTitleLoop; 
            END IF;
            UPDATE books SET title = 'Lorem ipsum dolor sit amet' WHERE books.id = book_id;
        SET rowCountTitle = rowCountTitle + 1;
      END LOOP;
  END;
  CLOSE updateTitle;

  SELECT 'number of titles updated =', rowCountTitle, 'number of descriptions updated =', rowCountDescription;
END

Я знаю, что вы нашли лучшее решение, но я считаю, что ответ на ваш исходный вопрос заключается в том, что вам нужно установить Done=0; между двумя курсорами, иначе второй курсор будет получать только одну запись перед выходом из цикла из-за Done=1 из предыдущего обработчика.


Я, наконец, написать другую функцию, которая делает то же самое:

DROP PROCEDURE IF EXISTS addNewFriend;
DELIMITER //
CREATE PROCEDURE addNewFriend(IN inUserId INT UNSIGNED, IN inFriendEmail VARCHAR(80))
BEGIN
 SET @tempFriendId = (SELECT id FROM users WHERE email = inFriendEmail);
 SET @tempUsersFriendsUserId = (SELECT user_id FROM users_friends WHERE user_id = inUserId AND friend_id = @tempFriendId);
 IF @tempFriendId IS NOT NULL AND @tempUsersFriendsUserId IS NULL THEN
  INSERT INTO users_friends (user_id, friend_id) VALUES(inUserId, @tempFriendId);
 END IF;
 SELECT @tempFriendId as friendId;
END //
DELIMITER ;

Я надеюсь, что это лучшее решение, оно все равно отлично работает. Спасибо, что сказал мне не использовать курсоры, когда это не нужно.


вместо использования курсоров для проверки наличия записей можно использовать предложение EXISTS в предложении WHERE:

INSERT INTO users_friends 
  (user_id, friend_id) 
VALUES
  (inUserId, tempFriendId)
WHERE EXISTS(SELECT NULL 
               FROM users 
              WHERE email = inFriendEmail)
  AND NOT EXISTS(SELECT NULL 
                   FROM users_friends 
                  WHERE user_id = tempFriendId 
                    AND friend_id = tempFriendId);

Я сделал изменение после прочтения комментариев пола о втором запросе и изменил логику, чтобы вставка не добавляла дубликаты. В идеале это должно быть обработано как первичный ключ, являющийся составным ключом (включая два или более столбцов), который остановит необходимость проверки кода.


Вау, я не знаю, что сказать, пожалуйста, идите и прочитайте и немного изучите sql, без обид, но это один из худших SQL, которые я когда-либо казался.

SQL-это язык, основанный на множестве, курсоры, в общем, плохие, бывают ситуации, когда они полезны, но они довольно редки. Использование курсоров здесь совершенно неуместно.

ваша логика во втором курсоре также ошибочна, так как она выберет любую запись, которая мешает другу, а не только требуемую дружба.

Если вы хотите исправить это, вы можете попробовать дать второму курсору другое имя, но предпочтительно начать все сначала.

установите составное PK или уникальное ограничение на users_friends, тогда вам не нужно беспокоиться о проверке отношений, а затем попробуйте что-то вроде этого.

INSERT INTO users_friends 
SELECT 
    @inUserId, 
    users.user_id
FROM 
    users
WHERE
    email = @inFriendEmail