You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
These are ranked and thus preference should be given—per the "shift left" approach—for validation to occur at the lowest level possible to ensure consistency, adherence of the DRY principle, etc.
It's worth noting that validation at the database level is nuanced given how different databases handle NULL values from a distinct uniqueness perspective. A great example of this is ensuring that a dataset is unique—in terms of the (schema, table_name) tuple—where an explicit UNIQUE constraint is missing, given that (as mentioned),
The reason it does not physically exist is MySQL, PostgreSQL, etc. have a different interpretation of uniqueness when it comes to NULL which is problematic given the schema is optional.
i.e., in MySQL the (NULL, foo) and (NULL, foo) tuples are perceived as being different, yet from a dataset perspective we would perceive these as being the same object.
The main challenges with our current approach are:
We tend to specify/enforce non-business validation logic at the DAO or Command level.
We tend to "ask for permission" as opposed to "ask for forgiveness" which is both inefficient (due to the redundancy of first checking and then actioning) and—when combined with (1)—does not protect against a potential race condition.
The validate() method tends to augment state—likely to address the inefficiency of (2)—which is an anti-pattern.
Examples
The following examples illustrate where the validation occurs at the wrong place:
The TagDAO.validate_tag_name() method checks for invalid characters yet this can be handled by either a CHECK constraint or SQLAlchemy validates() decorator.
There exists pairs of methods, i.e., DashboardDAO.validate_slug_uniqueness() and DashboardDAO.validate_update_slug_uniqueness(), for validating uniqueness used for creating and updating an entity respectively where the later (rightfully) includes an additional filter to exclude itself. This differentiation (and thus added complexity) is not required if there existed a corresponding UNIQUE constraint combined with the “ask for forgiveness” approach.
The following examples illustrate where the validation checks could be violated:
The ChartDAO.add_favorite() method first checks whether the user has favorited said chart and if not creates the favorite as opposed to simply trying to favorite the chart and relying on a UNIQUE constraint to block (at the database level) the creation if it already exists.
The following examples illustrate where validation augments state:
The UpdateDatabaseCommand.validate() method assigns the self._model attribute after fetching the model so it can be leveraged during the run phase.
The CreateChartCommand.validate() method assigns the self._model attribute and augments the self._properities attribute.
The ChartWarmUpCacheCommand.validate() method reassigns the self._chart_or_id attribute if the variable is an ID. Post validation the attribute name is misleading given it only pertains to a Chart object—hence this confusing statement.
The following examples illustrate inconsistencies between where validation occurs:
Validation should occur at the lowest possible (left most) level, given not all database operations originate at the DAO or Command level.
The DAO and Command should primarily be used to validate business logic, e.g., ownership, access controls, etc.
We should adopt a "ask for forgiveness" as opposed to "ask for permission" type approach by leveraging try/except blocks.
The BaseCommand.validate() method should be removed in favor of in-place validation, i.e., leveraging the “validate as you go” approach during the run() phase.
Examples
The following code illustrates how the model and DAO should be constructed, where it is evident that the model validation is handled at the database level and the DAO uses the “ask for forgiveness” approach*.
The try/except approach is based on the fact that the code block will mostly succeed (which is the case in this example given one can really only favorite an object which is unfavorited). Additionally this approach removes the redundant check of first checking whether said item has already been favorited by the user.
The following code illustrates the use of a CHECK constraint in conjunction with the “ask for forgiveness” approach.
[SIP-99C] Proposal for model and business validation
This SIP is part of the [SIP-99] Proposal for correctly handling business logic series. Specifically, it proposes formalizing where and how model and business validation should occur.
Validation
There are typically four places where validation can occur:
validates()
decoratorvalidate_*()
methodsBaseCommand.validate()
class methodThese are ranked and thus preference should be given—per the "shift left" approach—for validation to occur at the lowest level possible to ensure consistency, adherence of the DRY principle, etc.
It's worth noting that validation at the database level is nuanced given how different databases handle
NULL
values from a distinct uniqueness perspective. A great example of this is ensuring that a dataset is unique—in terms of the (schema
,table_name
) tuple—where an explicit UNIQUE constraint is missing, given that (as mentioned),i.e., in MySQL the (
NULL
,foo
) and (NULL
,foo
) tuples are perceived as being different, yet from a dataset perspective we would perceive these as being the same object.The main challenges with our current approach are:
validate()
method tends to augment state—likely to address the inefficiency of (2)—which is an anti-pattern.Examples
The following examples illustrate where the validation occurs at the wrong place:
user_id
,class_name
,obj_id
) tuple meaning that the uniqueness validation check is wrongfully handled by theChartDAO.favorited_ids()
andDashboardDAO.favorited_ids()
methods when invokingChartDAO.add_favorite()
,DashboardDAO.add_favorite()
, etc.short_descr
column meaning that the uniqueness validation check is wrongfully handled in theAnnotationDAO.validate_update_uniqueness()
method.TagDAO.validate_tag_name()
method checks for invalid characters yet this can be handled by either a CHECK constraint or SQLAlchemyvalidates()
decorator.DeleteChartCommand.validate()
method unnecessarily checks to see that there are no associated alerts or reports whereas it should rely solely on theReportSchedule.chart_id
FOREIGN KEY constraint.DashboardDAO.validate_slug_uniqueness()
andDashboardDAO.validate_update_slug_uniqueness()
, for validating uniqueness used for creating and updating an entity respectively where the later (rightfully) includes an additional filter to exclude itself. This differentiation (and thus added complexity) is not required if there existed a corresponding UNIQUE constraint combined with the “ask for forgiveness” approach.The following examples illustrate where the validation checks could be violated:
ChartDAO.add_favorite()
method first checks whether the user has favorited said chart and if not creates the favorite as opposed to simply trying to favorite the chart and relying on a UNIQUE constraint to block (at the database level) the creation if it already exists.The following examples illustrate where validation augments state:
UpdateDatabaseCommand.validate()
method assigns theself._model
attribute after fetching the model so it can be leveraged during the run phase.CreateChartCommand.validate()
method assigns theself._model
attribute and augments theself._properities
attribute.ChartWarmUpCacheCommand.validate()
method reassigns theself._chart_or_id
attribute if the variable is an ID. Post validation the attribute name is misleading given it only pertains to aChart
object—hence this confusing statement.The following examples illustrate inconsistencies between where validation occurs:
ChartDataCommand.run()
method does not invoke the siblingChartDataCommand.validate()
method, yet theCreateChartCommand.run()
method does invoke the siblingCreateChartCommand.validate()
method, i.e., due to inconsistencies, it is not apparent whether the Commandrun()
method will invoke the correspondingvalidate()
method.Proposed Change
try
/except
blocks.BaseCommand.validate()
method should be removed in favor of in-place validation, i.e., leveraging the “validate as you go” approach during therun()
phase.Examples
The following code illustrates how the model and DAO should be constructed, where it is evident that the model validation is handled at the database level and the DAO uses the “ask for forgiveness” approach*.
try
/except
approach is based on the fact that the code block will mostly succeed (which is the case in this example given one can really only favorite an object which is unfavorited). Additionally this approach removes the redundant check of first checking whether said item has already been favorited by the user.The following code illustrates the use of a CHECK constraint in conjunction with the “ask for forgiveness” approach.
New or Changed Public Interfaces
None.
New Dependencies
None.
Migration Plan and Compatibility
Rejected Alternatives
None.
The text was updated successfully, but these errors were encountered: