2

What would be best practice to handle removal of db row that has FK REFERENCE constraint? My goal was to present more user-friendly error messages to the end-user. Note that I dont want to delete department with employees and that I dont want to have cascade delete on tables.

For example if we have two tables:

-- Department table
CREATE TABLE [dbo].[Department](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 CONSTRAINT [PK_Department] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

-- Employee table
CREATE TABLE [dbo].[Employee](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
    [DepartmentId] [int] NULL,
 CONSTRAINT [PK_Employee] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

GO
ALTER TABLE [dbo].[Employee]  WITH CHECK ADD  CONSTRAINT [FK_Employee_Department] FOREIGN KEY([DepartmentId])
REFERENCES [dbo].[Department] ([Id])
GO
ALTER TABLE [dbo].[Employee] CHECK CONSTRAINT [FK_Employee_Department]

And if one wants to delete row from department table, where this row is referenced in employee table. What should one do?

  1. Before executing DELETE statement, check if row is referenced in employee table and gracefully return error to GUI (with eployee list if necessary)

  2. Execute DELETE statement and do catch exception like:

    catch (SqlException ex) 
    { 
        switch (ex.Number) 
           case 547: HandleErrorGracefully()
    } 
    
  3. Some other way?

Would be nice if someone have code/link to application sample ...

Ivan Milosavljevic
  • 839
  • 1
  • 7
  • 19
  • Possible duplicate http://stackoverflow.com/questions/7944559/how-to-manipulate-sqlexception-message-into-user-friendly-message – Shantanu Gupta Jan 18 '12 at 12:51

4 Answers4

6

Option 3: Do both 1 and 2

There is the potential for someone to insert in another process between the check (passes) and the delete failing.

In this case, you can just say "sorry, something went wrong" to the user (but log it), and see if they want to try again. Then the check wil intercept it.

gbn
  • 422,506
  • 82
  • 585
  • 676
  • I like option 3 but I don't think it should be exercised in every case: if it were then every database constraint would be re-implemented in 'front end' code, thereby increasing risk that they are not semantically equivalent, maintenance overhead of having to change code in two places, etc. Ideally both should use the same rules engine :) – onedaywhen Jan 18 '12 at 13:50
  • @onedaywhen: by "check" I mean some kind of "IF NOT EXISTS.. DELETE", in a stored procedure perhaps – gbn Jan 18 '12 at 13:55
2

Be nicer!

Show the departments and number of employees in it, only enable delete option if number of employees is 0.

Rely on the referential integrity to stop coding errors, not user ones...

Tony Hopkinson
  • 20,172
  • 3
  • 31
  • 39
  • A visible count of zero may be out of date, so the DELETE will fail and checks need to be made (as per my answer) – gbn Jan 18 '12 at 13:12
  • 1
    "Rely on the referential integrity to stop coding errors" -- not to be relied on! Incorrect implementation (or omission) of business rules into database constraints is itself a coding error. – onedaywhen Jan 18 '12 at 13:44
  • @gbn. Of course it may be out of date. Someone else could already have deleted the entire thing! How likely is that? Is it worth teh effort of the extra code to make nice with Foreign Key violation, if you have ameliorated.. – Tony Hopkinson Jan 18 '12 at 18:46
  • @onedaywhen, if I'm doing a db. First I get it right then I code, If while coding I discover it's not right, I stop, I fix , I retest I start again. My way you get data integrity, the other way, you get crossed fingers. – Tony Hopkinson Jan 18 '12 at 18:49
0

@gbn's answer is a good way to go. But you could also put ON DELETE CASCADE on the foreign key relation, and ask the user something like "Are you sure you want to delete this department and all its employees?".

Klaus Byskov Pedersen
  • 117,245
  • 29
  • 183
  • 222
0

I too had the same issue few days/months back. After lot of googling, I came to the resolution of translating error messages in DAL using SqlHelper class.

I handled this issue using the following way.

Created some classes that were derived from Exception class as shown in the diagram.

DalExceptionClass:Exception

ForeignKeyException: DalException

in SqlHelper

public void ExecuteNonQuery(string procedure)
{
    try
    {
      con.open()
       cmd.executenonquery();
      con.close();
      //Sql Exception is raised
    }
    catch(SqlException ex)
    {
        throw TranslateException(ex);
    }
}

using code in the link, I translated Sql Exceptions to DalException rather than Exception

protected DalException TranslateException(SqlException ex) { 
    DalException dalException = null; 

    // Return the first Custom exception thrown by a RAISERROR 
    foreach (SqlError error in ex.Errors) { 
        if (error.Number >= 50000) { 
            dalException = new DalException(error.Message, ex); 
        } 
    } 

    if (dalException == null) { 
        // uses SQLServer 2000 ErrorCodes 
        switch (ex.Number) { 
            case 2601: 
                // Unique Index/Constriant Violation 
                dalException = new DalUniqueConstraintException("{0} failed, {1} must be unique", ex); 
                break; 
            default: 
                // throw a general DAL Exception 
                dalException = new DalException(ex.Message, ex); 
                break; 
        } 
    } 

    // return the error 
    return dalException; 
} 

Please note case 2601:

I sent custom message here.

Now at my DAL, I will handle DalException only which will return me exception message as

"{0} failed, {1} must be unique" using string.Format(msg, "Insertion", "Client" );

this will send an exception to BAL as "Insertion failed, Client must be unique" which could somewhat be a granual message.

Edit

this will be handled at DAL as

    public Client Insert()
    {
    try
       {
        _repository.Insert(client);
       } 
    catch(DalException ex)
       {
//wrap message since, error will always be in client entity as client's method is being called. Also, insertion is being failed.
         throw new BusinessException(string.Format(ex.message), "Insertion", "Client");
       }
    }

References

How to manipulate SqlException message into user friendly message

http://www.reflectionit.nl/DAL.aspx

What should be the strategy to handle the sql exceptions?

Community
  • 1
  • 1
Shantanu Gupta
  • 20,688
  • 54
  • 182
  • 286
  • So, you will wait for exception and will not check/validate if row is referenced? Is there any reason for not doing so? – Ivan Milosavljevic Jan 18 '12 at 13:30
  • @IvanMilosavljevic: Yes, Since I will be having all the constraints in place and I will not be using Cascade since my design says not to delete dependent records automatically. So contstraints will prevent me from deleting a record that is being used. So if my intended goal was to delete record, So I will issue a delete query. If i really need to check if record is being used. Then It should be a seperate select query that will drive my validation business. – Shantanu Gupta Jan 18 '12 at 13:36
  • I know this might not be the best approach but using this I was able come close to more user friendly message. But we we migrates from Sql Server to some other server, then we might not get ErrorNumber, But that can be handled in TranslateException method only. Rest of the code will not get affected. – Shantanu Gupta Jan 18 '12 at 13:40