How often do you want to be sure values in your newly inserted records are unique? I just counted in my current project: 28 times. That’s a lot of repetitive code if you validate this requirement each time, so it makes sense to use some kind of uniqueness validator in cbvalidation. In older releases of cbvalidation there only was a unique validator for ORM which looks like this:

{ 
    fieldName : { validator: "UniqueValidator@cborm" },
    // or
    fieldName : { "UniqueValidator@cborm" : {}  }
}

So pretty easy, you don’t have to specify tablenames, fieldnames or primary keys. That’s only possible if you are using ORM entities, because they have all database information included in the entity definition. So if you want to use request collection validation you are out of luck( in a previous post I explained why this might be a good idea ).

In v2.1 a Unique validator was added but to be honest I think it is quite basic: you can only validate inserts and not updates, and there’s no way to use it with a non-default datasource. So time to create our own and a good time to explore all parameters of a customvalidator validate() call. I explained in this post how you can build a customValidator but in the current post I will elaborate on all extra parameters you can pass to your validator. And I will show some interesting differences and simularities of a request collection vs a model target in your validate call. I have some extra wishes to make this validator really better.

  1. as little configuration as possible. Well, we can’t ignore the database table holding my data, but when possible, I don’t want to configure more.
  2. suitable for inserts and updates. So that means for updates we need to exclude the current record. In that case I need to know the primary key name and value. Does this make it more complex? We will see.
  3. it should be usable both for models (easier) and request collections.
  4. the validation error messages should be configurable. cbValidation already has some standard support for this, but in the current cbValidation it is broken for customValidators. So we should fix that.

Ok, I will name my validator QUniqueValidator, just because UniqueValidator is already in use. My favourite constraint would look like this:

{ 
  myProperty : { "QUniqueValidator" : {tablename: "someTable"} }
}
// and if we really need more flexibility we could expand to
{ 
  myProperty : { 
    "QUniqueValidator" : {
      tableName: "someTable",
      targetColumn: "datebaseFieldNotEqualToPropertyName",
      keyProperty: "keyPropertyName", //default = "id"
      keyColumn: "keyColumnNameInDatabase", //default = "id"
      dataSource: "optionalDataSourceName",
      isLoaded: true/false //so you can override isLoaded detection
    } 
  }
}

To quickly recap what you need to create a custom validator: It is just a simple component which has a name property and a getName() function, a constructor and a validate function which returns true or false depending on the validation result. Almost all validate functions have two parts: the first part where your constraints are validated and a second part where you create your error messages.

The validate function has the following parameters

  • validationResult: a validationResult object which can be empty or already filled by other validators. If your customValidator fails to validate, you should add your validationErrors to this validationResult.
  • target: the target you are validating. When validating your model or request collection or a struct you would expect your arguments.target is a model or a struct. We will discuss this later, as this target name is a bit deceiving.
  • field: the model property or struct key you are trying to validate
  • targetValue: the value of this field
  • validationData: all extra info which is necessary to make your validator work. Old style validators use strings to supply data e.g range = 1..5 but newer validators use structs as in this customValidator or even a User Defined Function (the UDF validator). If you want to use custom errormessages the current cbvalidation library (v2.2) will fail for most validators which use a struct for validationData. If you really need these custom messages there’s a workaround for that.

My QUnique validator has a lot of these ValidationData properties, but only one is required:

  • tableName: if you want to find out if a value is already present in a table, you’ll need a tableName, a targetColumn and an arguments.targetValue
  • targetColumn: by default your targetColumn in your table has the same name as your arguments.field. Only if they are not the same you have to specify this column
  • keyProperty: if you are not only checking newly inserted records but also updated record you have to exclude the updated record itself. You can only do that if you know the keyProperty (id) of your model or struct.
  • keyColumn: you also have to know the key column name in your database. By default it is the named the same as your keyProperty. By knowing your keyProperty you can get the value of your Id and exclude your currently updated record from your uniqueness check.
  • dataSource: very optional. If you specify a datasource it will be added to your query options, otherwise the default datasource will be used
  • isLoaded: By default isLoaded will be determined by checking if you have a valid ID value in your model or struct. If you don’t want to rely on that you can override this by isLoaded = true|false for updates and inserts respectively.

Ok. Show us some code! I will, and below the code I will explain why this code will work for both model and struct validation. And I will give some hints on how to change your error messages if you need multiple languages.

boolean function validate(
  required  validationResult,
  required any target,
  required string field,
  any targetValue,
  any validationData
) {
  // validationData.tableName (required)
  param validationData.targetColumn = arguments.field;
  param validationData.keyProperty  = "id";
  param validationData.keyColumn    = validationData.keyProperty;
  // validationData.dataSource optional
  // validationData.isLoaded to override isLoaded detection

  var sql     = "Select * from #validationData.tableName# where #validationData.targetColumn# = :name";
  //prepare query params
  var params  = { 
    name : arguments.targetValue
  };

  // add datasource property, or use default
  var options = arguments.validationdata.keyExists("datasource") ? { datasource: arguments.validationdata.datasource } : {}; 
  
  // now get IDvalue or null if there's no ID value yet
  var IdValue = invoke( target, "get#validationData.keyProperty#" );

  // isLoaded: if not present detect isLoaded by valid ID which has some length
  var isLoaded = !arguments.validationData.KeyExists("isLoaded") ?  !IsNull(idValue) && len(IdValue) : arguments.validationData.isLoaded;

  // add datasource property, or use default
  if ( isLoaded ) {
    //add extra param and sql to exclude current record
    params.id = IdValue;
    sql &= " and #validationData.keyColumn# <> :id"
  }
  var result = queryExecute( sql, params, options )
  if ( !result.recordCount ) return true;

  // construction of your validation error messages
  var args = {
    message        : "The '#arguments.field#' value '#arguments.targetValue#' is not unique",
    field          : arguments.field,
    validationType : getName(),
    rejectedValue  : ( isSimpleValue( arguments.targetValue ) ? arguments.targetValue : "" ),
    validationData : arguments.validationData
  };
  validationResult.addError( 
    validationResult
      .newError( argumentCollection = args )
      .setErrorMetadata( { QUniqueValidator : arguments.validationData 
    } )
  );
  return false;
}

Validating a model vs a struct

Near line 25 of this code you will see I try to get an IdValue by calling invoke( target, "get#validationData.keyProperty#" ). But you might wonder: I can validate models or structs, at least that’s what the documentation tells me. If I call validate() or validateOrFail() on the validation manager these methods need a target, which can be an object or a struct. My validation is executed for every single property in my constraints so for the QUniqueValidator of myField the validation manager is passing data to the validate call of QUniqueValidator including the whole validation target (all struct fields or the model object). But what you can’t see: the validationmanager does a nice trick and converts all incoming structs to a generic object. Some missing method magic makes that you can call all keys as getters, so there’s no distinction in writing this validator for structs or model objects. You only have to realise it is quite a simple object with some getters. So for more complex business rule validations you probably have to rely on model validations. But for our QUniqueValidator it is fine, as long as we pass all relevant data including primary key values in our struct.

There’s another difference between struct and model validation: the return type.
The validate method of your validationManager will return a validationResult, but the validateOrFailMethod will return your object if your input target is an object. If your target is a struct your validationManager will return a struct of ALL validated fields. So this is a very powerful way to cleanup your input. You can feed your request collection to your validateOrFail call and if it doesn’t fail it will return a nice struct of only the fields which were defined in your constraints. This struct can be used to populate your objects, without long exclusion lists of “dangerous” properties. If you need some property for population which needs no validation, you could add a simpleField: { required=false } to your constraints. This way it is quite easy to do a double validation: first validate your request collection, use the result to populate your object and then validate additional (business) rules.

Showing custom error messages

cbValidation has some very basic way to return custom error messages. If your validator is called foo, you can create a fooMessage key for your field which will return a custom error message, e.g

constraints = {
  "myField": { 
    required= true, requiredMessage="Oops, required",
    foo = "", fooMessage="This is foo!"
  }
}

Unfortunately, this mechanism breaks for any customValidator which has a struct of ValidationData, such as our QUniqueValidator. I am sure this problem will finally be resolved with a library update, but until then there are two workarounds. Near line 41 my message is defined as message : "The '#arguments.field#' value '#arguments.targetValue#' is not unique" . We could add a message property to our validation data, so we could just call message: arguments.ValidationData.message or we could read it from a resource file as used in cbi18n which is already part of cbvalidation.

There’s a lot more which can be done with customValidators. We use it for checking password complexity, and -as a hosting company – many, many validations of IP numbers, Ipv6 numbers and Ip ranges which should be part of other ranges. I’m sure you can think of your own scenario’s and if things are unclear, just leave me a note here or at the cfml slack channel.