74 votes

Passage d'un tableau de paramètres à une procédure stockée

J'ai besoin de passer un tableau de "id" à une procédure stockée, pour supprimer toutes les lignes de la table SAUF les lignes qui correspondent aux id dans le tableau.

Comment puis-je le faire de la manière la plus simple qui soit ?

2 votes

Ne s'agit-il pas d'une duplication de stackoverflow.com/questions/114504/ ?

3 votes

@John Saunders, il y a beaucoup de questions sur le serveur sql "passer un tableau comme paramètre". Cependant, celle-ci a une particularité : elle supprime toutes les lignes, sauf la partie de la question concernant les paramètres passés. Par conséquent, je ne pense pas qu'il s'agisse d'un doublon.

0 votes

John Saunders, je ne sais pas. J'ai fait une recherche mais je n'ai pas trouvé ce que je cherchais. Y a-t-il un problème avec cela ?

56voto

rmiesen Points 911

Si vous utilisez Sql Server 2008 ou une version plus récente, vous pouvez utiliser un paramètre évalué par une table (TVP) au lieu de sérialiser et de désérialiser les données de votre liste chaque fois que vous voulez les transmettre à une procédure stockée.

Commençons par créer un schéma simple qui nous servira de terrain de jeu :

CREATE DATABASE [TestbedDb]
GO

USE [TestbedDb]
GO

    /* First, setup the sample program's account & credentials*/
CREATE LOGIN [testbedUser] WITH PASSWORD=N'µ×?
?S[°¿Q­¥½q?_I¼Ð)3õl%dv', DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=ON
GO

CREATE USER [testbedUser] FOR LOGIN [testbedUser] WITH DEFAULT_SCHEMA=[dbo]
GO

EXEC sp_addrolemember N'db_owner', N'testbedUser'
GO

    /* Now setup the schema */
CREATE TABLE dbo.Table1 ( t1Id INT NOT NULL PRIMARY KEY );
GO

INSERT INTO dbo.Table1 (t1Id)
VALUES
    (1),
    (2),
    (3),
    (4),
    (5),
    (6),
    (7),
    (8),
    (9),
    (10);
GO

Avec notre schéma et nos données d'exemple en place, nous sommes maintenant prêts à créer notre procédure stockée TVP :

CREATE TYPE T1Ids AS Table (
        t1Id INT
);
GO

CREATE PROCEDURE dbo.FindMatchingRowsInTable1( @Table1Ids AS T1Ids READONLY )
AS
BEGIN
        SET NOCOUNT ON;

        SELECT Table1.t1Id FROM dbo.Table1 AS Table1
        JOIN @Table1Ids AS paramTable1Ids ON Table1.t1Id = paramTable1Ids.t1Id;
END
GO

Une fois notre schéma et notre API en place, nous pouvons appeler la procédure stockée TVP à partir de notre programme comme suit :

        // Curry the TVP data
        DataTable t1Ids = new DataTable( );
        t1Ids.Columns.Add( "t1Id",
                           typeof( int ) );

        int[] listOfIdsToFind = new[] {1, 5, 9};
        foreach ( int id in listOfIdsToFind )
        {
            t1Ids.Rows.Add( id );
        }
        // Prepare the connection details
        SqlConnection testbedConnection =
                new SqlConnection(
                        @"Data Source=.\SQLExpress;Initial Catalog=TestbedDb;Persist Security Info=True;User ID=testbedUser;Password=letmein12;Connect Timeout=5" );

        try
        {
            testbedConnection.Open( );

            // Prepare a call to the stored procedure
            SqlCommand findMatchingRowsInTable1 = new SqlCommand( "dbo.FindMatchingRowsInTable1",
                                                                  testbedConnection );
            findMatchingRowsInTable1.CommandType = CommandType.StoredProcedure;

            // Curry up the TVP parameter
            SqlParameter sqlParameter = new SqlParameter( "Table1Ids",
                                                          t1Ids );
            findMatchingRowsInTable1.Parameters.Add( sqlParameter );

            // Execute the stored procedure
            SqlDataReader sqlDataReader = findMatchingRowsInTable1.ExecuteReader( );

            while ( sqlDataReader.Read( ) )
            {
                Console.WriteLine( "Matching t1ID: {0}",
                                   sqlDataReader[ "t1Id" ] );
            }
        }
        catch ( Exception e )
        {
            Console.WriteLine( e.ToString( ) );
        }
  /* Output:
   * Matching t1ID: 1
   * Matching t1ID: 5
   * Matching t1ID: 9
   */

Il existe probablement un moyen moins pénible de le faire en utilisant une API plus abstraite, comme Entity Framework. Cependant, je n'ai pas le temps de le vérifier par moi-même pour le moment.

0 votes

Je pense que c'est la meilleure façon de faire. Je suis choqué que tu n'aies pas eu de vote pour ça. Donc je t'en ai donné un.

1 votes

J'aime cette approche. Merci de partager.

0 votes

Pour la réutilisation, je pense que c'est de loin la meilleure approche. Prenons l'exemple suivant d'une application courante dans le monde réel : J'ai une recherche, qui génère un ensemble de résultats comprenant une clé primaire unique. Je souhaite ensuite transmettre cet ensemble de résultats à une autre opération (par exemple, récupérer des informations supplémentaires pour une feuille de calcul Excel). Avec cette méthode, je peux facilement récupérer une liste d'identifiants à partir du jeu de résultats initial et passer la liste dans ma procédure stockée de seconde phase qui accepte un paramètre unique de "@IDs T1IDs READONLY". Ainsi, j'évite la lourde charge de la recherche initiale.

43voto

Zanoni Points 8401

Utilisez une procédure stockée :

EDIT : Un complément pour sérialiser List (ou n'importe quoi d'autre) :

List<string> testList = new List<int>();

testList.Add(1);
testList.Add(2);
testList.Add(3);

XmlSerializer xs = new XmlSerializer(typeof(List<int>));
MemoryStream ms = new MemoryStream();
xs.Serialize(ms, testList);

string resultXML = UTF8Encoding.UTF8.GetString(ms.ToArray());

Le résultat (prêt à être utilisé avec le paramètre XML) :

<?xml version="1.0"?>
<ArrayOfInt xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <int>1</int>
  <int>2</int>
  <int>3</int>
</ArrayOfInt>

POSTE ORIGINAL :

Passage de XML comme paramètre :

<ids>
    <id>1</id>
    <id>2</id>
</ids>

CREATE PROCEDURE [dbo].[DeleteAllData]
(
    @XMLDoc XML
)
AS
BEGIN

DECLARE @handle INT

EXEC sp_xml_preparedocument @handle OUTPUT, @XMLDoc

DELETE FROM
    YOURTABLE
WHERE
    YOUR_ID_COLUMN NOT IN (
        SELECT * FROM OPENXML (@handle, '/ids/id') WITH (id INT '.') 
    )
EXEC sp_xml_removedocument @handle

4 votes

+1 C'est une excellente solution si vos données se trouvent déjà dans une structure XML, mais ce n'est pas le cas si vous ajoutez les frais de création du XML côté client.

1 votes

Comme RolandTumble l'a mentionné, cela nécessitera de convertir mon tableau de données d'entrée (liste) en XML, donc je pense que ce n'est pas la meilleure solution dans mon cas.

0 votes

Vous pourriez utiliser Serialize pour le faire... Il est plus fiable d'utiliser le XML que de diviser les chaînes de caractères...

20voto

KM. Points 51800

C'est la meilleure source :

http://www.sommarskog.se/arrays-in-sql.html

créer une fonction de fractionnement en utilisant le lien, et l'utiliser comme suit :

DELETE YourTable
    FROM YourTable                           d
    LEFT OUTER JOIN dbo.splitFunction(@Parameter) s ON d.ID=s.Value
    WHERE s.Value IS NULL

Je préfère l'approche des tableaux de chiffres

Voici un code basé sur le lien ci-dessus qui devrait le faire pour vous...

Avant d'utiliser ma fonction, vous devez configurer une table "auxiliaire", vous ne devez le faire qu'une seule fois par base de données :

CREATE TABLE Numbers
(Number int  NOT NULL,
    CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number ASC)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
DECLARE @x int
SET @x=0
WHILE @x<8000
BEGIN
    SET @x=@x+1
    INSERT INTO Numbers VALUES (@x)
END

Utilisez cette fonction pour diviser votre chaîne de caractères, qui ne tourne pas en boucle et est très rapide :

CREATE FUNCTION [dbo].[FN_ListToTable]
(
     @SplitOn              char(1)              --REQUIRED, the character to split the @List string on
    ,@List                 varchar(8000)        --REQUIRED, the list to split apart
)
RETURNS
@ParsedList table
(
    ListValue varchar(500)
)
AS
BEGIN

/**
Takes the given @List string and splits it apart based on the given @SplitOn character.
A table is returned, one row per split item, with a column name "ListValue".
This function workes for fixed or variable lenght items.
Empty and null items will not be included in the results set.

Returns a table, one row per item in the list, with a column name "ListValue"

EXAMPLE:
----------
SELECT * FROM dbo.FN_ListToTable(',','1,12,123,1234,54321,6,A,*,|||,,,,B')

    returns:
        ListValue  
        -----------
        1
        12
        123
        1234
        54321
        6
        A
        *
        |||
        B

        (10 row(s) affected)

**/

----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
INSERT INTO @ParsedList
        (ListValue)
    SELECT
        ListValue
        FROM (SELECT
                  LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(@SplitOn, List2, number+1)-number - 1))) AS ListValue
                  FROM (
                           SELECT @SplitOn + @List + @SplitOn AS List2
                       ) AS dt
                      INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
                  WHERE SUBSTRING(List2, number, 1) = @SplitOn
             ) dt2
        WHERE ListValue IS NOT NULL AND ListValue!=''

RETURN

END --Function FN_ListToTable

vous pouvez utiliser cette fonction comme une table dans une jointure :

SELECT
    Col1, COl2, Col3...
    FROM  YourTable
        INNER JOIN dbo.FN_ListToTable(',',@YourString) s ON  YourTable.ID = s.ListValue

voici votre suppression :

DELETE YourTable
    FROM YourTable                                d
    LEFT OUTER JOIN dbo.FN_ListToTable(',',@Parameter) s ON d.ID=s.ListValue
    WHERE s.ListValue IS NULL

0 votes

@KM : Pourquoi le maximum de 8000 caractères ? Peut-on utiliser des chaînes de caractères plus longues que cela ?

0 votes

Vous pouvez faire en sorte que le paramètre d'entrée soit de type varchar(max), mais assurez-vous que votre table Numbers comporte suffisamment de lignes. J'utilise simplement 8000, car je ne divise jamais de longues chaînes de caractères. Vous pouvez consulter cette autre réponse de ma part : stackoverflow.com/questions/4227552/ où j'ai mis à jour la fonction de fractionnement pour utiliser une fonction de table et j'ai également amélioré la génération de la table Numbers.

12voto

astander Points 83138

Vous pouvez essayer ceci :

DECLARE @List VARCHAR(MAX)

SELECT @List = '1,2,3,4,5,6,7,8'

EXEC(
'DELETE
FROM TABLE
WHERE ID NOT IN (' + @List + ')'
)

6 votes

... tant que vous êtes sûr que le paramètre @List n'est pas rempli à partir d'une entrée utilisateur.

0 votes

La beauté est la simplicité.

3voto

Simon Points 101

Vous pouvez utiliser une table temporaire dont la procédure stockée s'attend à ce qu'elle existe. Cela fonctionnera sur les anciennes versions de SQL Server, qui ne prennent pas en charge le XML, etc.

CREATE TABLE #temp
(INT myid)
GO
CREATE PROC myproc
AS
BEGIN
    DELETE YourTable
    FROM YourTable                    
    LEFT OUTER JOIN #temp T ON T.myid=s.id
    WHERE s.id IS NULL
END

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X