Click here to Skip to main content
15,881,898 members
Articles / Database Development / SQL Server
Article

Delete and Constraint Handling Stored Procedure Using SQL Server 2000

Rate me:
Please Sign up or sign in to vote.
3.33/5 (3 votes)
25 Oct 20063 min read 52.3K   212   19   6
A generalized stored procedure that performs constraint checks before performing deletes.

Introduction

I was providing consulting services, developing an application in C# and .NET 2.0 with a SQL Server 2000 database. The application framework was based on a custom code-generator, which created SQL scripts, business objects, and UI code. Because the previous consultant who designed the code-generator was no longer available, time constraints restricted us from discovering and modifying the generator, leading me to take a different approach to my solutions.

In this article, I describe how I created the ability to perform deletes. The framework did not include proper scripts for cleaning up and deleting records, and for reasons beyond the discussion of this article, I chose to do all of the constraint checks and deletes from a stored procedure, and return an error message if any constraint violations would occur from a delete.

SQL Source Code

Adding a DROP and ADD command at the beginning of the script makes it easy to update this stored procedure without having to create an accompanying ALTER script.

SQL
if exists (select * from dbo.sysobjects where 
           id = object_id(N'[dbo].[DeleteByTableAndId]') 
           and OBJECTPROPERTY(id, N'IsProcedure') = 1)
 drop procedure [dbo].[DeleteByTableAndId]
GO

Adding a DROP and ADD command for the custom error message that will be raised if constraints will be violated with a deletion ensures that the error message will fit the way we will use it. Notice the ‘%s’ which acts as a string place holder for the error message.

SQL
IF(EXISTS (SELECT * FROM master.dbo.sysmessages WHERE error = 50001)) 
BEGIN
 EXECUTE sp_dropmessage 50001;
END
GO
SQL
EXECUTE sp_addmessage 50001, 18, '%s';
GO

Included pieces of the system stored procedure sp_fkeys, and made the input parameters for table name, primary key ID, and bit flags for removing relationships and performing deletes.

SQL
CREATE PROCEDURE DeleteByTableAndId(
      @pktable_name  sysname,
      @object_id   int,
      @remove_relationships  bit=0,
      @perform_delete  bit=0)

Included the field in #fkeysout that indicates if the foreign key is nullable.

SQL
create table #fkeysout(
   PKTABLE_QUALIFIER sysname collate database_default NULL,
   PKTABLE_OWNER sysname collate database_default NULL,
   PKTABLE_NAME sysname collate database_default NOT NULL,
   PKCOLUMN_NAME sysname collate database_default NOT NULL,
   FKTABLE_QUALIFIER sysname collate database_default NULL,
   FKTABLE_OWNER sysname collate database_default NULL,
   FKTABLE_NAME sysname collate database_default NOT NULL,
   FKCOLUMN_NAME sysname collate database_default NOT NULL,
   KEY_SEQ smallint NOT NULL,
   UPDATE_RULE smallint NULL,
   DELETE_RULE smallint NULL,
   FK_NAME sysname collate database_default NULL,
   PK_NAME sysname collate database_default NULL,
   DEFERRABILITY smallint null,
          ISNULLABLE smallint null)
SQL
insert into #fkeysout
  select
   PKTABLE_QUALIFIER = convert(sysname,db_name()),
   PKTABLE_OWNER = convert(sysname,USER_NAME(o1.uid)),
   PKTABLE_NAME = convert(sysname,o1.name),
   PKCOLUMN_NAME = convert(sysname,c1.name),
   FKTABLE_QUALIFIER = convert(sysname,db_name()),
   FKTABLE_OWNER = convert(sysname,USER_NAME(o2.uid)),
   FKTABLE_NAME = convert(sysname,o2.name),
   FKCOLUMN_NAME = convert(sysname,c2.name),
   KEY_SEQ,
   UPDATE_RULE = CASE WHEN (ObjectProperty(fk_id, 
                 'CnstIsUpdateCascade')=1) THEN 
    convert(smallint,0) ELSE convert(smallint,1) END,
   DELETE_RULE = CASE WHEN (ObjectProperty(fk_id, 
                 'CnstIsDeleteCascade')=1) THEN 
    convert(smallint,0) ELSE convert(smallint,1) END,
   FK_NAME = convert(sysname,OBJECT_NAME(fk_id)),
   PK_NAME,
   DEFERRABILITY = 7,
          ISNULLABLE = convert(smallint,c2.isnullable)
  from #fkeys f,
   sysobjects o1, sysobjects o2,
   syscolumns c1, syscolumns c2
  where o1.id = f.pktable_id
   AND o2.id = f.fktable_id
   AND c1.id = f.pktable_id
   AND c2.id = f.fktable_id
   AND c1.colid = f.pkcolid
   AND c2.colid = f.fkcolid

The first section of the custom code contains declarations for the SQL execute string and the string that will hold return values from sp_executesql.

SQL
DECLARE @sql_string nvarchar(4000) 
SET @sql_string = ''
DECLARE @return NVARCHAR(4000)
SET @return = ''

If the table being deleted has no constraints, skip to DeleteLabel.

SQL
IF(NOT EXISTS (SELECT * FROM #fkeysout))
 GOTO DeleteLabel

If the parameter was not set to nullify the constraint, check all of the relationships. Otherwise, only check the non-nullable relationships. The conditional clause is basically a SQL string builder that selects from #fkeysout, and keeps us from using a looping cursor. This will produce an inline SQL statement that assigns the result to the variable declared above. The end result will be a comma-separated string like this:

,TABLE01(5),TABLE02(12),TABLE03(1)

SQL
IF(@remove_relationships = 0) 
 SELECT @sql_string = @sql_string + ' ISNULL((SELECT '',' + 
        FKTABLE_NAME + '('' + CONVERT(varchar, COUNT(*)) + 
        '')'' AS [TABLE] FROM [' + FKTABLE_NAME + 
        '] WHERE [' + FKCOLUMN_NAME + '] = ' + 
        CONVERT(varchar,@object_id)  + ' GROUP BY [' +
        FKCOLUMN_NAME + ']), '''')+'
 FROM #fkeysout
ELSE 
 SELECT @sql_string = @sql_string + ' ISNULL((SELECT '',' + 
        FKTABLE_NAME + '('' + CONVERT(varchar, COUNT(*)) + 
        '')'' AS [TABLE] FROM [' +
        FKTABLE_NAME + '] WHERE [' + FKCOLUMN_NAME + 
        '] = ' + CONVERT(varchar,@object_id)  + ' GROUP BY [' +
        FKCOLUMN_NAME + ']), '''')+'
 FROM #fkeysout
 WHERE ISNULLABLE = 0  
SQL
SET @sql_string = 'SELECT' + @sql_string
SET @sql_string = LEFT(@sql_string, LEN(@sql_string)-1)
SET @sql_string = 'SELECT @RETURN_OUT = (' + @sql_string + ');'
SQL
EXECUTE sp_executesql @sql_string, 
        N'@RETURN_OUT NVARCHAR(4000) OUTPUT', 
        @return_out = @return OUTPUT

If a return exists, then irresolvable constraints were found. Format the string to include the primary key table name and the result string, removing the beginning comma.

Note: SQL Server 2000 has a hard length restriction of 399 characters for error messages, so I resorted away from using XML. Ideally, I would prefer to short-circuit the process and return XML to the caller. If XML existed, the caller would process this as if an error had been raised.

The existing result will look like this:

{TABLEPK}TABLE01(5),TABLE02(12),TABLE03(1)
SQL
IF(LEN(@return)>0) 
BEGIN
 SET @return = '{' + @pktable_name + '}' + 
               RIGHT(@return, LEN(@return)-1)
 
 RAISERROR (50001,18,1,@return)
END

The first labeled section cleans up the relationships by nulling-out the fields. Here, I use the same SQL string builder approach.

SQL
ResolveLabel:
IF(@remove_relationships = 1 AND LEN(@return)=0) 
 BEGIN
  SET @sql_string = ''
  
  SELECT @sql_string = @sql_string + ' UPDATE [' + 
      FKTABLE_NAME + '] SET [' + 
      FKCOLUMN_NAME + '] = NULL WHERE ['
      + FKCOLUMN_NAME + '] = ' + 
      CONVERT(nvarchar,@object_id)  + ';'
  FROM #fkeysout
  WHERE ISNULLABLE = 1
  
  EXECUTE(@sql_string)
 END 

The final labeled section is for deleting the actual record from the primary key table. First, check to see if the table selected has a composite primary key, and return an error if it does, since this stored procedure is only designed to manage tables with constraints. Then, retrieve the primary key column name from the metadata. Finally, build and perform the SQL  statement.

SQL
DeleteLabel:
IF(@perform_delete = 1 AND LEN(@return)=0) 
BEGIN <CODE lang=sql>DELETE</CODE>
 SET @sql_string = ''
 SET @return = ''


 DECLARE @return_count int
 SET @return_count = 0
 
 SET @sql_string = 'SELECT @RETURN_COUNT_OUT = 
                   (SELECT COUNT(*) FROM 
                   INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
                   WHERE TABLE_NAME = ''' + @pktable_name + 
                   ''' AND CHARINDEX(''PK_'',CONSTRAINT_NAME)>0)'
 EXECUTE sp_executesql @sql_string, 
         N'@RETURN_COUNT_OUT INT OUTPUT', 
         @return_count_out = @return_count OUTPUT
 IF(@return_count <> 1)
 BEGIN
  RAISERROR('Cannot delete this record because it 
             does not have only one identifier.',18,1);
 END
 ELSE
 BEGIN
  SET @sql_string = 'SELECT @RETURN_OUT = (SELECT COLUMN_NAME 
                     FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
                     WHERE TABLE_NAME = ''' + @pktable_name + 
                     ''' AND CHARINDEX(''PK_'',CONSTRAINT_NAME)>0)' 
  EXECUTE sp_executesql @sql_string, N'@RETURN_OUT 
          NVARCHAR(4000) OUTPUT', @return_out = @return OUTPUT
  SET @sql_string = 'DELETE [' + @pktable_name + '] WHERE [' + 
                    @return + '] = ' + CONVERT(nvarchar,@object_id) + ';' 
  EXECUTE sp_executesql @sql_string
 END
END

A few improvements could be adding the ability to perform cascaded DELETEs and return XML instead of an error message. I tend to stay away from cascading DELETEs, because they can be dangerous and might spawn an application time-out.

Error Class

I also included a complimenting error class coded in C# 2.0 that handles all of the processing for the custom SQL error raised.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace Acme.Common.Exceptions
{
    public class ConstraintException : System.Exception
    {
        public ConstraintException(string message)
            : base(FixMessage(message))
        {
        }
        public ConstraintException(string message, 
                                   System.Exception ex)
            : base(FixMessage(message), ex)
        {
        }
        private static string FixMessage(string msg)
        {
            try
            {
                if (msg != string.Empty)
                {
                    int index = msg.LastIndexOf(")");
                    if (index != msg.Length - 1)
                    {
                        msg = msg.Substring(0, 
                                  msg.Length - index + 1);
                    }
                }
            }
            catch
            {
                msg = string.Empty;
            }
            return msg;
        }
        public string Primary
        {
            get
            {
                try
                {
                    Regex regex = new Regex(@"\{(?<primary>" + 
                                            @".*)\}(?<relationships>.*)");
                    return regex.Replace(Message, "${primary}");
                }
                catch
                {
                    return string.Empty;
                }
            }
        }
        public List<ConstraintRelationship> Relationships
        {
            get
            {
                List<ConstraintRelationship> list = 
                      new List<ConstraintRelationship>();
                try
                {
                    Regex regex = new Regex(@"\{(?<primary>" + 
                                            @".*)\}(?<relationships>.*)");
                    string[] relationships = regex.Replace(Message, 
                                             "${relationships}").Split(',');
                    for (int i = 0; i < relationships.Length; i++)
                    {
                        Regex regex2 = new Regex(@"(?<relationship>" + 
                                                 @".*)\((?<count>.[0-9]*)\)");
                        string name = regex2.Replace(relationships[i], 
                                                     "${relationship}");
                        string count = regex2.Replace(relationships[i], 
                                                      "${count}");
                        list.Add(new ConstraintRelationship(name, 
                                 Convert.ToInt32(count)));
                    }
                    list.Sort();
                }
                catch (Exception ex)
                { }
                return list;
            }
        }
        public class ConstraintRelationship : IComparable
        {
            public string Name;
            public int Count;
            public ConstraintRelationship(string name, int count)
            {
                Name = name;
                Count = count;
            }
            #region IComparable Members
            public int CompareTo(object obj)
            {
                if (obj is ConstraintRelationship)
                {
                    ConstraintRelationship cr = (ConstraintRelationship)obj;
                    return Name.CompareTo(cr.Name);
                }
                throw new ArgumentException("object is " + 
                          "not a Constraint Relationship");
            }
            #endregion
        }
    }
}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Canada Canada
Jeremy Mullinax-Hill is currently working as a .NET Programmer for the IT consulting division of a multi-national Fortune 100 corporation. His expertise is in C#, ASP.NET, and SQL Server. He has been programming since 1998, and prefers an environment that requires using a vast array of skills including architecture, database, design, and programming.

Jeremy lives in a suburb north of Atlanta, GA, and spends most of his free time with his wife, son, and newborn twin boys. He enjoys reading fantasy and horror novels, AD&D-based role-playing games, warfare and RPG video games, and guitar.

Comments and Discussions

 
GeneralPing Back Pin
Jeremy Mullinax-Hill12-Apr-10 17:51
Jeremy Mullinax-Hill12-Apr-10 17:51 
Generaljunk Pin
WebMaster26-Oct-06 2:40
WebMaster26-Oct-06 2:40 
AnswerRe: junk [modified] Pin
Jeremy Mullinax-Hill26-Oct-06 4:15
Jeremy Mullinax-Hill26-Oct-06 4:15 
GeneralRe: junk Pin
Jasmine25011-Nov-06 5:21
Jasmine25011-Nov-06 5:21 
That makes sense. I was about to write about how dumb I thought this was, but you are right. You should have put that clarification in the original article, cuz on the face of it, it seems like you did a lot of work just to do something that SQL Server does anyway.

This is an example of the problems created by dynamic code though. A little thing like foreign key violations which normally wouldn't be a problem, is a huge big deal when dynamically generated code is involved. Sounds like the previous consultant created a general design flaw. I've never seen a problem that had to be solved by dynamic code, outside the realm of development tools, that is. Sometimes you gotta think about it a little bit, but that's our job.

"Quality Software since 1983!"
http://www.smoothjazzy.com/

GeneralRe: junk Pin
Jeremy Mullinax-Hill2-Nov-06 4:12
Jeremy Mullinax-Hill2-Nov-06 4:12 
GeneralRe: junk Pin
Jasmine25012-Nov-06 6:05
Jasmine25012-Nov-06 6:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.