Here is an example of my dilema - binding values to a database query parameter; three possible error hierarchies can be invoked:
The programmer provided an incorrect index or bind name
SqlException->SqlDatabaseException->SqlProgrammingException
Some database problem prevented the bind
SqlException->SqlDatabaseException->SqlOperationalException
The database interface library caused a problem
SqlException->SqlInterfaceException
I want to supply the following values - Bind index, upper bound for arguments, value to be bound.
Normally, when there is only one hierarchy, I would create a subclass with the required instance variables and a custom toString (or equivalent) implementation. With three hierarchies, I would have to create three subclasses for the same variables, a bit of a waste.
So I decided to do something a bit more generic. I created my base exception class with the concept of properties so you have something like this (pseudo-ish D code):
class Exception {
typeof(this) setProperty (T)(string key, T value)
Box getProperty (key)
bool hasProperty (key)
Box[string] propertyBag
}
typeof(this) setProperty (T)(string key, T value)
Box getProperty (key)
bool hasProperty (key)
Box[string] propertyBag
}
The setProperty returns the type of the exception to allow for chained method calling.
Since index range errors are common developer errors, I decided to create a exception class for that case so I have:
SqlException
->SqlDatabaseException
->SqlProgrammingException->SqlBindException
->SqlOperationalException
->SqlInterfaceException
For the standard bind error, I instantiate the SqlBindException as such:
throw (new SqlBindException ("Bind index out of range"))
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText);
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText);
If there is a database error, then I can do the following:
throw (new SqlOperationalException ("Something went wrong"))
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText)
.setProperty ("VendorCode", vendorCode)
.setProperty ("VendorMsg", vendorMsg);
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText)
.setProperty ("VendorCode", vendorCode)
.setProperty ("VendorMsg", vendorMsg);
or if the interface is in an inconsistent state with the database:
throw (new SqlInterfaceException ("The interface is broken"))
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText)
.setProperty ("VendorCode", vendorCode)
.setProperty ("VendorMsg", vendorMsg);
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText)
.setProperty ("VendorCode", vendorCode)
.setProperty ("VendorMsg", vendorMsg);
With the modified toString, I get the following:
SqlBindException: Bind index out of range
BindPosition: 4
BindType: TEXT
BindUBound: 3
BindValue: testval
BindValueType: string
SQL: select ... from ... where x=?, y=?, z=?;
BindPosition: 4
BindType: TEXT
BindUBound: 3
BindValue: testval
BindValueType: string
SQL: select ... from ... where x=?, y=?, z=?;
By separating the properties from the message, I have absolute control over how I will display, or log, the information.
And that helps with the other problem: long messages versus terse description.
The above SqlBindException has a very terse description, the one that is common to sequence type errors. I have seen many instances of 'array bounds out of range' type errors. Of course, with the properties, the message can be terse but the information necessary to track the bug is available.
Let's say, I have the following terse error message:
"Failed to bind parameter"
Not very descriptive. Now let's say that I wanted to provide a default user facing error message for developers to use, or a more descriptive message aimed at developers.
"Failed to bind "~bindType~" parameter, either the statement is finalized, or the database is in an inconsistent state. If the statement is finalized, then the interface is out of sync with the database. If the database is in an inconsistent state, then this may be an interface error or a problem with SQLite. If the problem can be replicated, please log an issue with a test case."
To support long messages, I have added them to the base class:
class Exception {
typeof(this) setProperty (T)(string key, T value)
Box getProperty (key)
bool hasProperty (key)
Box[string] propertyBag
typeof(this) setLongMsg (string longMsg)
string longMsg;
}
typeof(this) setProperty (T)(string key, T value)
Box getProperty (key)
bool hasProperty (key)
Box[string] propertyBag
typeof(this) setLongMsg (string longMsg)
string longMsg;
}
Besides the length of message, long messages differ from the normal exception messages in that they support properties with the "$()" syntax. The above message is set as a long message as follows:
throw (new SqlOperationalException ("Failed to bind parameter"))
.setLongMsg (
"Failed to bind $(BindType) parameter, either the statement "
"is finalized, or the database is in an inconsistent "
"state. If the statement is finalized, then the interface "
"is out of sync with the database. If the database is in "
"an inconsistent state, then this may be an interface "
"error or a problem with SQLite. If the problem can be "
"replicated, please log an issue with a test case.")
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText)
.setProperty ("VendorCode", vendorCode)
.setProperty ("VendorMsg", vendorMsg);
.setLongMsg (
"Failed to bind $(BindType) parameter, either the statement "
"is finalized, or the database is in an inconsistent "
"state. If the statement is finalized, then the interface "
"is out of sync with the database. If the database is in "
"an inconsistent state, then this may be an interface "
"error or a problem with SQLite. If the problem can be "
"replicated, please log an issue with a test case.")
.setProperty ("BindPosition", idx)
.setProperty ("BindType", "TEXT")
.setProperty ("BindUBound", ubound)
.setProperty ("BindValue", value)
.setProperty ("BindValueType", valueType)
.setProperty ("SQL", sqlText)
.setProperty ("VendorCode", vendorCode)
.setProperty ("VendorMsg", vendorMsg);
When the instance property longMsg is invoked, the message is returned as follows:
Failed to bind TEXT parameter, either the statement is finalized, or the database is in an inconsistent state. If the statement is finalized, then the interface is out of sync with the database. If the database is in an inconsistent state, then this may be an interface error or a problem with SQLite. If the problem can be replicated, please log an issue with a test case.
Properties allow me to do something a little different with long error messages. When chaining exceptions, I can construct a long message that uses properties from the chained exception. Eg:
try {
throw (new SomeException ("Inner exception"))
.setProperty ("A", aValue);
} catch (SomeException e) {
throw (new OtherException ("Outer exception", e))
.setLongMsg (
"Blah blah blah $(A) blah blah $(B).")
.setProperty ("B", bValue);
}
throw (new SomeException ("Inner exception"))
.setProperty ("A", aValue);
} catch (SomeException e) {
throw (new OtherException ("Outer exception", e))
.setLongMsg (
"Blah blah blah $(A) blah blah $(B).")
.setProperty ("B", bValue);
}
I have found that the concept of exception properties to be a valuable tool for exception flexibility; something that is making my life a bit easier.
No comments:
Post a Comment