2

DML is not allowed in a Visualforce page controller's constructor. However, a colleague and I discovered that the behavior with various DML-equivalent ("DML-ish"?) code is inconsistent and rather strange.

Here's a minimal Visualforce page:

<apex:page controller="TestBatchFiringController">
</apex:page>

and its corresponding controller:

public class TestBatchFiringController {
    public TestBatchFiringController() {
        // These statements successfully completed.

        // Results in a valid AsyncApexJob id, but batch does not execute.
        // AsyncApexJob Id is not valid after method executes (but it is in test context!) 
        System.debug('My job id is: ' + Database.executeBatch(new TestBatchFiringBatch(), 1));

        // Results in a valid CronTrigger id, but schedulable does not execute.
        // CronTrigger Id is not valid after method executes (but it is in test context!) 
        System.debug('My schedulable job id is ' + System.schedule('Test Job', '20 30 8 10 2 ?', new TestSchedulableFiring()));

        // This would fail with a `LimitException` - DML is not allowed.
        // Account a = new Account(Name = 'Testy Test Test');
        // insert a;
        // System.debug('My Account\'s Id is ' + a.Id);

        // Setting a savepoint is DML-equivalent - this also throws a `LimitException`
        // System.Savepoint sp = Database.setSavepoint();

    }
}

Performing DML results in just the exception expected, as does setting a save point. Neither are allowed in this context.

However, the behavior of invoking Schedulable or Batchable classes from a Visualforce controller is stranger. Both Database.executeBatch() and System.schedule() execute successfully, and each returns an apparently valid Id value for an AsyncApexJob or CronTrigger respectively. (The Ids can be System.debug()ed, and are visible in the logs). These objects, however, don't exist following the completion of the controller, and the asynchronous jobs do not execute. In the batch case, neither start() nor execute() is called.

This behavior is unique to the code being run as the controller of the Visualforce page. In a test context, with the controller being instantiated directly, both asynchronous jobs complete successfully, and the AsyncApexJob and CronTrigger records can be inspected afterwards.

Both batchable and schedulable jobs can be successfully invoked from an action method, including when the action method is fired by <apex:page action="{! myAction }"> upon page load.

We're not going to proceed with a design that does this, of course, but I'd like to understand better what's happening.

Is the Visualforce controller's constructor actually wrapped in a save point behind the scenes with a rollback occurring silently? Is there some reason that a Schedulable or Batchable behaves differently than other DML-ish operations? Is this divergence of behavior documented anywhere?

4
  • what is the purpose of writing that kind of code in constructor? you can use that in page action – Santanu Boral May 16 '18 at 18:22
  • Is the behaviour different when you move the logic to a method and invoke it by visualforce's action attribute? Usually action is recommended for performing DML operations. – Raul May 16 '18 at 18:23
  • 2
    @SantanuBoral, it was an experiment on the part of a colleague - we changed the architecture to avoid this issue. Raul, yes, both operations complete normally in a Visualforce action method, including when invoked from the page's action attribute. – David Reed May 16 '18 at 18:26
  • What I understand with this behavior is that the DML you are trying to execute is in the "same context" and will utilize the resources within the same transaction and thus is restricted by design. However invoking anything as "batch or future" really won't be in the same transaction context, but it will "schedule" it, and its the platform which will determine based on the availability of resources as when to execute those scheduled processes. This is the only thing I can think of. – Jayant Das May 16 '18 at 18:28
1

Related Question.


However, that's hardly a very fox-like answer, is it?

From what I can tell, during the constructor's call, the Visualforce runtime has not fully initialized yet. The reason why you can't do DML is because it would crash. And, as you've observed, even if you find holes in the armor, it still doesn't work, because the system state isn't initialized yet.

As a fun little example, consider this code:

public class q218357 {
    public q218357() {
        System.assert(false);
    }
}

When we load the page, we get:

Assertion Failed

An unexpected error has occurred. Your development organization has been notified.

However, if we move it to an action:

public class q218357 {
    public q218357() {
    }
    public void action() {
        System.assert(false);
    }
}

The output is different:

Assertion Failed

Error is in expression '{!action}' in component in page q218357: Class.q218357.action: line 5, column 1

An unexpected error has occurred. Your development organization has been notified.

As you can see, the Visualforce runtime can't even produce a stack trace for the assertion when it happens in the constructor, but it has no problem dealing with it (i.e. Visualforce actually displays the correct error message) in the second form. Pretty much anything Bad in your constructor will cause a different path of exceptions to occur. It is expected that you will not modify the database in any way, will not throw any exceptions, and basically do nothing more than simple, initializing logic.

Why this happens is a mystery to us outsiders, but you should know that the Visualforce runtime is an interesting, if not quirky, little beast. Every time you see a situation where you can't use some feature of Visualforce, such as PageReference.getContent() not working in a unit test, it's because the runtime doesn't support trying to do whatever it is you're doing in that context.

There's a separate team for Apex Code and Visualforce, so whenever you see Apex Code that looks like it's part of Visualforce, the Apex Code team isn't the one that wrote the underlying library. Most likely, DML in the constructor is just one of those things that doesn't work right. We're not told why this has to be, just understand that the reason is likely technical in nature.

Looking at the logs for the various execution paths, we see that Visualforce itself starts initializing by allocating some heap and calling our constructor:

17:00:36.0 (5381165)|VF_APEX_CALL_START|[EXTERNAL]|01p50000004Y2Ua|q218357 <init>
  ...
17:00:36.0 (51615657)|VF_APEX_CALL_END|q218357 <init>

During this time, Visualforce is not initialized, for the same reason why you're not allowed to do certain things in Java in a constructor, because the system isn't initialized yet. It causes Bad Things™ to happen.

While I doubt you'll get a more official answer than this without signing an NDA, all of the evidence suggests that the system simply can't handle it because the runtime hasn't initialized by that point in time.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.