I’m implementing ‘check’ constraints that simply call a CLR function for each constrained column.
Each CLR function is one or two lines of code that attempts to construct an instance of the user-defined C# data class associated with that column. For example, a ‘Score’ class has a constructor which throws a meaningful error message when construction fails (i.e. when the score is outside a valid range).
First, what do you think of that approach? For me, it centralizes my data types in C#, making them available throughout my application, while also enforcing the same constraints within the database, so it prevents invalid manual edits in management studio that non-programmers may try to make. It’s working well so far, although updating the assembly causes constraints to be disabled, requiring a recheck of all constraints (which is perfectly reasonable). I use DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS to make sure the data in all tables is still valid for enabled and disabled constraints, making corrections as necessary, until there are no errors. Then I re-enable the constraints on all the tables via ALTER TABLE [tablename] WITH CHECK CHECK CONSTRAINT ALL. Is there a T-SQL statement to re-enable with check all check constraints on ALL tables, or do I have to re-enable them table by table?
Finally, for the CLR functions used in the check constraints, I can either:
- Include a try/catch in each function to catch data construction errors, returning false on error, and true on success, so that the CLR doesn’t raise an error in the database engine, or…
- Leave out the try/catch, just construct the instance and return true, allowing that aforementioned ‘meaningful’ error message to be raised in the database engine.
I prefer 2, because my functions are simpler without the error code, and when someone using management studio makes an invalid column edit, they’ll get the meaningful message from the CLR like 'Value for type X didn't match regular expression '^p[1-9]\d?$'' instead of some generic SQL error like ‘constraint violated’. Are there any severe negative consequences of allowing CLR errors through to SQL Server, or is it just like any other insert/update failure resulting from a constraint violation?
It worries me a bit, because calling a ctor requires memory allocation, which is relatively expensive. For each row inserted, you’re calling a ctor — and only for its side-effects.
Also expensive are exceptions. They’re great when you need them, but this is a case where you vould use them in a ctor context, but not in a check context.
A refactoring could reduce both costs, by having the check exist as a class static or free function, then both the check constraint and the ctor could call that:
Check constraint calls Score::valid(), no construction or exception needed.
Of course, you still have the overhead, for each row, of a CLR call. Whether that’s acceptable is something you’ll have to decide.
No, but you can do this to generate the commands:
and then run the resultset against the database.
Comments from the OP:
I’m not sure I agree with the exception throwing, but again, the "take-home message" is that by decomposing the problem into parts, you can select what parts you’re wiling to pay for, without paying for parts you don’t use. The ctor you don’t use, because you were only calling it to get the side-effect. So we decomposed creation and checking. We can further decompose throwing:
Now a user can call the ctor, and get the check and possible exception and construction, call checkValid and get the check and exception, or isValid to just get the validity, paying the runtime cost for only what he needs.