Próba zapisania obiektu naruszającego więzy integralności przy użyciu biblioteki NHibernate spowoduje wygenerowanie wyjątku GenericADOException. Dopiero sięgając do wartości właściwości InnerException możemy przekonać się, co jest przyczyną niepowodzenia. Istnieje jednak sposób na zastąpienie standardowego wyjątku własnym.

W przykładzie użyję prostej klasy:

public class User 
{ 
    public virtual int Id { get; set; } 
    public virtual string UserName { get; set; } 
    public virtual int Age { get; set; } 
}

i mapowania:

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        Id(x => x.Id);
        Map(x => x.UserName).Unique();
        Map(x => x.Age); 
    }
}

Próba zapisu dwóch użytkowników o tej samej nazwie kończy się wyjątkiem GenericADOException:

[Test, ExpectedException(typeof(NHibernate.Exceptions.GenericADOException))]
public void CannotAddTwoUsersWithTheSameUsername()
{
    var janKowalski1 = new User {UserName = "Jan Kowalski"};
    var janKowalski2 = new User {UserName = "Jan Kowalski"};

    using (var session = DataAccessFacade.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        session.Save(janKowalski1);
        session.Save(janKowalski2);
        tx.Commit();
    }
}

Oczywiście można sprawdzić, co jest przyczyną błędu przy zapisie:

var janKowalski1 = new User {UserName = "Jan Kowalski"};
var janKowalski2 = new User {UserName = "Jan Kowalski"};

using (var session = DataAccessFacade.OpenSession())
using (var tx = session.BeginTransaction())
{
    try
    {
        session.Save(janKowalski1);
        session.Save(janKowalski2);
        tx.Commit();
    }
    catch (GenericADOException ex)
    {
        if (ex.InnerException is SQLiteException)
        {
            var sqLiteException = (SQLiteException) ex.InnerException;
            switch(sqLiteException.ErrorCode)
            {
                case SQLiteErrorCode.Constraint:
                    // do something with constaint exception
                default:
                    throw;
            }
        }
        throw;
    }
}

Jednak ten sposób uzależniamy nasz kod od konkretnego providera. Ewentualna zmiana silnika bazy danych powoduje konieczność zmian w wielu klasach warstwy dostępu do danych. Poza tym obsługa wydawałoby się trywialnego problemu wymaga każdorazowo napisania wielu linii kodu.

Czy powyższy fragment nie mógłby wyglądać tak:

var janKowalski1 = new User {UserName = "Jan Kowalski"};
var janKowalski2 = new User {UserName = "Jan Kowalski"};

using (var session = DataAccessFacade.OpenSession())
using (var tx = session.BeginTransaction())
{
    try
    {
        session.Save(janKowalski1);
        session.Save(janKowalski2);
        tx.Commit();
    }
    catch (ConstraintViolationException ex)
    {
        // do something with constaint exception
    }
}

O ile krócej, prawda? I nie obchodzi nas, że w danej chwili wykorzystujemy bazę danych SQLite, Oracle czy MSSQL Server.

Aby osiągnąć ten efekt używając NHibernate, musimy stworzyć własną implementację interfejsu ISQLExceptionConverter:

public class MyCustomSQLExceptionConverter: ISQLExceptionConverter
{
    public Exception Convert(AdoExceptionContextInfo adoExceptionContextInfo)
    {
        var ex = ADOExceptionHelper.ExtractDbException(
            adoExceptionContextInfo.SqlException) as SQLiteException;
        if (ex != null)
        {
            switch (ex.ErrorCode)
            {
                case SQLiteErrorCode.Constraint:
                    return 
                        new NHibernate.Exceptions.ConstraintViolationException(
                            adoExceptionContextInfo.Message,
                            ex.InnerException,
                            adoExceptionContextInfo.Sql,
                            null
                        );
            }
        }
        return SQLStateConverter.HandledNonSpecificException(
            adoExceptionContextInfo.SqlException,
            adoExceptionContextInfo.Message,
            adoExceptionContextInfo.Sql
        );
    }
}

Metoda ADOExceptionHelper.ExtractDbException wyłuskuje pierwszy wyjątek System.Data.Common.DbException z drzewa wraz z jego InnerExceptionami. Reszta jest chyba oczywista.

Konfigurujemy jeszcze NHibernate w pliku konfiguracyjnym:

<property name="sql_exception_converter">
    NameSpace.MyCustomSQLExceptionConverter, AssemblyName
</property>

albo za pomocą Fluent NHibernate:

_configuration.SetProperty(
    NHibernate.Cfg.Environment.SqlExceptionConverter,
    typeof(MyCustomSQLExceptionConverter).AssemblyQualifiedName
);