@Indexable Rules For The Road

The @Indexable annotation has rules that must be followed for the index updating to occur as expected.

Recently when I was working on my custom Headless API blog series, I ran into a bit of trouble with my Service Builder-based persistence tier.

My SB code was done and working, and I was adding methods to my CLI tool to test all of the Headless methods.

I had the list working, I could add, update and patch Vitamins, and I just finished the delete method testing and I was on cloud nine...

Everything was working! Great! I took one more stroll through the CLI to see that the commands were going to work after adding some shortcuts...

I went through the following sequence:

  1. Add a vitamin.
  2. Add a vitamin.
  3. List.
  4. Add a vitamin.
  5. Add a vitamin.
  6. List.
  7. Patch a vitamin.
  8. Put a vitamin.
  9. List.
  10. Delete a vitamin.
  11. List.
  12. Wham! No entity found with primary key 105!

What was going on? I mean, I'm just doing a listing, how does a listing result in an exception for the "No entity found with primary key"?

So I thought back to the section of the blog for building the Listing method: https://liferay.dev/blogs/-/blogs/creating-headless-apis-part-4#implementing-getvitaminspage

I remembered how the listing was actually going to be doing an index search, and the last function reference passed to the Liferay implementation was a function to use the primary key to lookup the PersistedVitamin object.

So basically I had done a delete but then the list used the index which must have returned a document for the deleted vitamin, the function failed to find a value and the exception was thrown.

This meant that my delete was removing the record from the database, but it was not updating the index even though I thought my annotations were correct.

And (with some help from friends) I realized I had broken the rules for the @Indexable annotation...

So I thought I'd do a quick post to refresh us on the rules so maybe I'll remember not to break them again...

Rule #1 - Methods Must Return an Entity

This one may sound kind of weird, but it is important. The Liferay code that applies the indexing AOP wrapper comes from the IndexableAdvice class, and this class checks the method return type to ensure it is returning something that extends BaseModel (all entities extend BaseModel).

So you can't define your method like:

@Indexable(type=IndexableType.DELETE)
public void deleteVitamin(PersistedVitamin vitamin) {...}

Even though you might not need or want the deleted entity, without returning the entity the AOP aspect won't wrap the method and wouldn't delete the document from the index when the entity is deleted.

You might ask why this is? By returning the deleted entry, the wrapping AOP logic can get the entity id and use that to find the appropriate document to remove from the index. If the entity is not returned, it doesn't have visibility on the entity to remove from the index.

Rule #2 - Method Entry Points Must Be Hit

So this rule is based on how transactions are applied to Service Builder methods...

Transactions are applied on the entry point into your SB layers, so you'll get a read/write or a read-only transaction on the main entry point and this will be inherited by most other internal method calls.

So if you called your SB method "getAndDeleteEntity()", this gets wrapped in a read-only transaction (because of the "get" prefix) and any deletions occurring inside of the method would get lost.

Additionally, because of how AOP would wrap the service entry point, not the actual method in the XxxLocalServiceImpl class itself, you may need to go through an injected XxxLocalService to hit the method, just to make sure the AOP aspect is applied on exit from the actual call.

So, for example, my PersistedVitaminLocalServiceImpl can have a deleteVitamin() method where I might want to do things like drop resource actions, etc. But if I end the method with:

return super.deletePersistedVitamin(vitamin);

My entity would be deleted, but the AOP aspects are not wrapped around the "super" object, they are wrapped around the actual service instance.

I have the actual local service instance @Referenced in for my by the superclass, so if I change my ending line to:

return persistedVitaminLocalService.deletePersistedVitamin(vitamin);

This time since I'm going through the actual service, the AOP aspect has wrapped the service and my entity will be deleted and the aspect used to delete the entity's document from the index.

Conclusion

Well, those are the two rules. If you follow the rules, your index updating annotations will apply correctly.

I know I focused here on the delete type, but the same rules apply to the update type used on add and update methods in the service tier.

Blogs

Thanks, Dave. This pairs nicely with our official docs coverage: https://portal.liferay.dev/docs/7-2/frameworks/-/knowledge_base/f/model-entity-indexing-framework#annotating-service-methods-to-trigger-indexing

Yeah, that documentation was great and how I applied the annotations to my 7.2 code. I really fell short on rule #1, but this cascaded into rule #2 since my internal method calls were using this.deleteVitamin() methods, so I was failing twice...  Once I figured out where I had gone sideways (with help from friends), I realized I had broken both the rules even though on retrospect the doco already spelled most of it out...