miércoles, 14 de enero de 2015

Copiar filas en la misma tabla solo cambiando el Id (TSQL)

Vamos a analizar diferentes escenarios en los cuales necesitamos copiar/clonar filas de una tabla a esa misma tabla, Solo cambiando el valor de un campo, Que en este caso sera el campo Id, Usando Microsoft SQL Server.

Supongamos que tenemos el AntId (Id original Id de el que queremos copiar las filas, en este caso este Id puede estar en varias filas o solo en una) y el NuevoId ( El Id nuevo que queremos poner en las filas que vamos a copiar/clonar)

Caso 1 - conocemos el nombre de la tabla y de todos sus campos:

El primer caso es de una tabla de la cual sabemos de antemano su nombre y su estructura, así que podemos escribir una consulta como la siguiente:

DECLARE @OldId int
DECLARE @NuevoId int
SET @AntId =13456 -- ejem.
SET @NuevoId =45687 -- ejem.

INSERT INTO MiTabla(Id,ColumnaA,ColumnaB,ColumnaC)
SELECT @NuevoId ,ColumnaA,ColumnaB,ColumnaC
FROM MyTable WHERE Id=@AntId 


Caso 2 - Conocemos el nombre de la tabla pero no de los campos:

En este caso nos vemos forzados a usar una tabla temporal para poder actualizar el ID antes de copiar las filas sin conocer el resto de las columnas:

DECLARE @AntId int
DECLARE @NuevoId int
SET @AntId =13456 -- ejm.
SET @NuevoId =45687 -- ejm.

--Copiar la filas que queremos a la tabla temporal
SELECT INTO #Temp FROM MiTabla WHERE Id=@AntId 

--Actualizar el Id en la tabla temporal    
UPDATE #Temp SET Id = @NuevoId 

--Copiar las filas con el nuevo Id de vuelta a MiTabla
INSERT INTO MiTabla  SELECT * FROM #Temp

--Borrar la tabla temporal     
if object_id(N'tempdb..#Temp'N'U'is not null  DROP TABLE #Temp
     
     
Caso 3 - No conocemos ni el nombre de la tabla ni de los campos:

Este escenario nos obliga a usar querys dinámicos para poder armar nuestra consulta con el nombre de la tabla, Nuestro primer intento seria algo así (y va a FALLAR)

DECLARE @NombreDeTabla varchar(32)
DECLARE @AntId int
DECLARE @NuevoId int

SET @NombreDeTabla ='MiTabla' --ejm podria llegar como parametro
SET @AntId =13456 -- ejm.
SET @NuevoId =45687 -- ejm.

SET @v_SQL = 'SELECT * INTO #Temp FROM ' + @NombreDeTabla ' WHERE Id='+  CAST(@AntId as varchar
EXEC(@v_SQL)
    
SET @v_SQL = 'UPDATE #Temp SET Id = '+  CAST(@NuevoId as varchar
EXEC(@v_SQL)

SET @v_SQL = 'INSERT INTO ' + @NombreDeTabla ' SELECT * FROM #Temp'
EXEC(@v_SQL)

--Borrar la tabla temporal    
SET @v_SQL = 'if object_id(N''tempdb..#Temp'', N''U'') is not null  DROP TABLE #Temp'
EXEC(@v_SQL)

     
Falla con errores porque el comando Exec() tienen su propio Scope ( que egoísta!) así que la tabla temporal no esta disponible para las siguientes sentencias.

La solución es usar una tabla temporal Global "##"  así estará disponible en el  Scope de el resto de los comandos Exec. 
Esto crea otro posible problema en sistemas multiusuario, porque se puede dar el caso que dos usuarios usen el proceso al mismo tiempo, y como la tabla temporal estaría disponible para ambos se pueden obtener errores o datos incorrectos. Así que necesitamos adicionar algo para que el nombre de la tabla temporal sea único para nuestro proceso, en este caso usaremos el nuevo Id que queremos usar para diferenciar el nombre de la tabla de otros usuarios concurrentes, Aunque puedes usar tu propio (y mas robusto) generador de Ids para estos casos si lo deseas.

La solución final queda así:

DECLARE @NombreDeTabla varchar(32)
DECLARE @AntId int
DECLARE @NuevoId int

SET @NombreDeTabla ='MiTabla' --ejm.
SET @AntId =13456 -- ejm.
SET @NuevoId =45687 -- ejm.

DECLARE @v_SQL varchar(1024)

SET @v_SQL = 'SELECT * INTO ##Temp'CAST(@NuevoId as varchar) +' FROM ' + @NombreDeTabla + ' WHERE Id = '+  CAST(@AntId as varchar
EXEC(@v_SQL)
    
SET @v_SQL = 'UPDATE ##Temp'CAST(@NuevoId as varchar) +' SET Id = '+  CAST(@NuevoId as varchar
EXEC(@v_SQL)

SET @v_SQL = 'INSERT INTO ' + @NombreDeTabla + ' SELECT * FROM ##Temp'CAST(@NuevoId as varchar)
EXEC(@v_SQL)
     
SET @v_SQL = 'if object_id(N''tempdb..##Temp'CAST(@NuevoId as varchar) +''', N''U'') is not null  DROP TABLE ##Temp'CAST(@NuevoId as varchar)  
EXEC(@v_SQL)
GO


Si lo ponemos como un Procedimiento Almacenado:

CREATE PROCEDURE dbo.CopiarConNuevoId
(
  @NombreDeTabla varchar(32),
  @NuevoId int,
  @AntId int
)
AS
  SET NOCOUNT ON
BEGIN
DECLARE @v_SQL varchar(1024)

SET @v_SQL = 'SELECT * INTO ##Temp'CAST(@NuevoId as varchar) +' FROM ' + @NombreDeTabla + ' WHERE Id = '+  CAST(@AntId as varchar
EXEC(@v_SQL)
    
SET @v_SQL = 'UPDATE ##Temp'CAST(@NuevoId as varchar) +' SET Id = '+  CAST(@NuevoId as varchar
EXEC(@v_SQL)

SET @v_SQL = 'INSERT INTO ' + @NombreDeTabla + ' SELECT * FROM ##Temp'CAST(@NuevoId as varchar)
EXEC(@v_SQL)
     
SET @v_SQL = 'if object_id(N''tempdb..##Temp'CAST(@NuevoId as varchar) +''', N''U'') is not null  DROP TABLE ##Temp'CAST(@NuevoId as varchar)  
EXEC(@v_SQL)
     
END
GO

Y eso es todo, espero que les sea de ayuda en alguna ocasión.

No hay comentarios:

Publicar un comentario