4

I have a Grails domain class. I would like to add a validation such that when the application is running in TEST environment, the domain class can't have more than 100 records.

I can't figure out the best way to add this type of validation. I am on Grails 2.5.3.

This is what I have so far.

class MyDomain {

String test

static constraints = {
   test blank: false, nullable: false
   id blank: false, validator: {value, command -> 
      if (Environment.current == Environment.TEST) {
          //do validation for not allowing more than 100 records
      }
   }
}

How can I add this validation?

Shashank Agrawal
  • 25,161
  • 11
  • 89
  • 121
Anthony
  • 33,838
  • 42
  • 169
  • 278

2 Answers2

2

Wouldn't something like this do the trick for you?

class MyDomain {

String test

static constraints = {
   test blank: false, nullable: false
   id blank: false, validator: {value, command -> 
      if (Environment.current == Environment.TEST) {
          //do validation for not allowing more than 100 records
          if (MyDomain.count() > 100) return ['too.many.records']
      }
   }
}
Joshua Moore
  • 24,706
  • 6
  • 50
  • 73
2

Solution for a single domain

What @Joshua answered is perfectly fine but there are few other ways. One of them is:

class MyDomain {

    String test

    void beforeInsert() {
        if (Environment.current == Environment.TEST) {
            MyDomain.withNewSession {
                if (MyDomain.count() == 100) {
                    throw new Exception("Not allowing more than 100 records")
                }
            }
        }
    }

    static constraints = {
        test blank: false
    }
}

Also, please note two things:

  1. The blank: false on the id field of no use since it is not a string because blank constraint is applicable on a String
  2. The nullable: false is of no use since the default value of nullable constraint is false

Generic solution for across domain TL;DR

If you want this behavior across multiple domains, copying the same code is not recommended as your code won't be DRY. For that, you can register a custom event listener:

First define a Java annotation in src/groovy:

import java.lang.annotation.Documented
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Documented
@Target([ElementType.TYPE, ElementType.FIELD])
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitRowsFoo {

    int value() default 100
}

Now define another Groovy class:

import grails.util.Environment
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener

class PreInsertEventListener extends AbstractPersistenceEventListener {

    PreUpdateEventListener(final Datastore datastore) {
        super(datastore)
    }

    @Override
    protected void onPersistenceEvent(AbstractPersistenceEvent event) {
        // Instance of domain which is being created
        Object domainInstance = event.entityObject

        if (Environment.current == Environment.TEST) {
            if (domainInstance.class.isAnnotationPresent(LimitRowsFoo.class)) {
                // Just using any domain reference here
                MyDomain.withNewTransaction {
                    int maxRows = domainInstance.class.getAnnotation(LimitRowsFoo.class).value()
                    if (domainInstance.class.count() == maxRows) {
                        event.cancel()
                    }
                }
            }
        }
    }

    @Override
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        // Only allow PreInsert event to be listened
        eventType.name == PreInsertEvent.class.name
    }
}

Now register this in Bootstrap.groovy:

application.mainContext.eventTriggeringInterceptor.datastores.each { k, datastore ->
        applicationContext.addApplicationListener new PreInsertEventListener(datastore)
    }

Now, in your domain, all you have to do is like these:

@LimitRowsFoo
class MyDomain {

    String test

    static constraints = {
        test blank: false
    }
}

@LimitRowsFoo(value = 200)   // to limit records to 200
class MyAnotherDomain {

    String name
}
Shashank Agrawal
  • 25,161
  • 11
  • 89
  • 121
  • 1
    Nicely done, but doesn't that pass validation and only fail on persistence events? It would make more sense if you were going to DRY it up to make a shared constraint instead of letting it get all the way down to the persistence layer before checking this. – Joshua Moore Dec 28 '16 at 18:23
  • Oh! yes. You are right. I never thought about the shared constraint. Good point. Thanks @JoshuaMoore – Shashank Agrawal Dec 28 '16 at 18:24
  • 1
    This is a great answer. Appreciate it. – Anthony Dec 28 '16 at 19:14