2012-06-14 6 views
11

He escrito una expresión CTE muy simple que recupera una lista de todos los grupos de los que un usuario es miembro.TSQL CTE: ¿Cómo evitar el recorrido circular?

Las reglas son así, un usuario puede estar en múltiples grupos y los grupos pueden anidarse para que un grupo pueda ser miembro de otro grupo, y además, los grupos pueden ser miembros mutuos de otro, entonces el Grupo A es un miembro del Grupo B y Grupo B es también un miembro del Grupo A.

Mi CTE es el siguiente y, obviamente, se produce una recursión infinita:

  ;WITH GetMembershipInfo(entityId) AS(-- entity can be a user or group 
       SELECT k.ID as entityId FROM entities k WHERE k.id = @userId 
       UNION ALL 
       SELECT k.id FROM entities k 
       JOIN Xrelationships kc on kc.entityId = k.entityId 
       JOIN GetMembershipInfo m on m.entityId = kc.ChildID 
      ) 

no puedo encontrar una solución fácil a Back rastrear esos grupos que ya he grabado.

Estaba pensando en usar un parámetro varchar adicional en el CTE para registrar una lista de todos los grupos que he visitado, pero usar varchar es demasiado crudo, ¿no?

¿Hay una manera mejor?

+0

¿Estás seguro de que es recurrente para siempre? El servidor predeterminado es 100 iteraciones. Intente leer la sugerencia de 'MAXRECURSION' en [MSDN] (http://msdn.microsoft.com/en-us/library/ms175972.aspx). – Bridge

+0

Primero preocúpese por la eficacia, * luego * preocúpese por la crudeza, si el tiempo lo permite :) – AakashM

+0

no se repite para siempre porque arroja un error después de 100 llamadas recursivas. Perdona mi redacción. – Haoest

Respuesta

25

Necesita acumular una cadena centinela dentro de su recursión. En el siguiente ejemplo tengo una relación circular de A, B, C, D, y luego de vuelta a A, y evito un bucle con la cadena de centinela:

DECLARE @MyTable TABLE(Parent CHAR(1), Child CHAR(1)); 

INSERT @MyTable VALUES('A', 'B'); 
INSERT @MyTable VALUES('B', 'C'); 
INSERT @MyTable VALUES('C', 'D'); 
INSERT @MyTable VALUES('D', 'A'); 

; WITH CTE (Parent, Child, Sentinel) AS (
    SELECT Parent, Child, Sentinel = CAST(Parent AS VARCHAR(MAX)) 
    FROM @MyTable 
    WHERE Parent = 'A' 
    UNION ALL 
    SELECT CTE.Child, t.Child, Sentinel + '|' + CTE.Child 
    FROM CTE 
    JOIN @MyTable t ON t.Parent = CTE.Child 
    WHERE CHARINDEX(CTE.Child,Sentinel)=0 
) 
SELECT * FROM CTE; 

Resultado:

Parent Child Sentinel 
------ ----- -------- 
A  B  A 
B  C  A|B 
C  D  A|B|C 
D  A  A|B|C|D 
+1

Me gusta su solución porque funciona. ¿Pero hay una manera de hacer esto sin una cadena de centinelas? Siento que es complicado y duplicado que tenemos que agregar algún tipo de delimitador alrededor de cada entrada centinela, digamos Sentinel = '<' + CAST (Parent AS VARCHAR (MAX)) + '>' Luego tenemos que hacer lo mismo en la función CharIndex(), porque sin los delimitadores puede haber falsos positivos. ¿Y qué ocurre si la cadena centinela es tan grande que excede la longitud de varchar (max)? – Haoest

+2

Me alegra oír que esto funciona. Es un truco, y sinceramente, no puedo pensar en una manera "más limpia". Sin embargo, tenga en cuenta que el centinela crece a lo largo de cada rama recursiva de forma independiente, por lo que solo será tan grande como la profundidad máxima por cada cadena, más el delimitador. VARCHAR (MAX) tiene un límite de 2 GB, mientras que la profundidad máxima se puede ampliar, si es necesario, hasta un máximo de 32767. Por lo tanto, es muy poco probable que se desborde VARCHAR (MAX). La mayoría de los trabajos de recursión pueden tener unos pocos miles de árboles, pero cuyas profundidades raramente exceden 5 o más. Entonces, sus cadenas centinelas generalmente serán bastante pequeñas. –

+0

bueno saberlo, gracias. – Haoest

2

En lugar de una cadena centinela, use una variable de tabla centinela. La función captará la referencia circular sin importar cuántos saltos haya en el círculo, sin problemas con la longitud máxima de nvarchar (máximo), fácilmente modificable para diferentes tipos de datos o incluso varias teclas, y puede asignar la función a una restricción de verificación.

CREATE FUNCTION [dbo].[AccountsCircular] (@AccountID UNIQUEIDENTIFIER) 
RETURNS BIT 
AS 
BEGIN 
    DECLARE @NextAccountID UNIQUEIDENTIFIER = NULL; 
    DECLARE @Sentinel TABLE 
    (
     ID UNIQUEIDENTIFIER 
    ) 
    INSERT INTO  @Sentinel 
       ([ID]) 
    VALUES   (@AccountID) 
    SET @NextAccountID = @AccountID; 

    WHILE @NextAccountID IS NOT NULL 
    BEGIN 
     SELECT @NextAccountID = [ParentAccountID] 
     FROM [dbo].[Accounts] 
     WHERE [AccountID] = @NextAccountID; 
     IF EXISTS(SELECT 1 FROM @Sentinel WHERE ID = @NextAccountID) 
      RETURN 1; 
     INSERT INTO @Sentinel 
       ([ID]) 
     VALUES  (@NextAccountID) 
    END 
    RETURN 0; 
END