Discover more about the most loved features of Sensei.
The Scope functionality of Sensei has always been a favorite of developers. With the ability to expand or limit the scope of a recipe’s application, development teams have been able to customize its usage to individual projects and verticals within their organizations - empowering developers to personalize their experience.
And understandably, it is at the center of Sensei’s continuous innovation processes. During an innovation brainstorming session on expanding the scope of “scope” (yes, pun intended), a question came up:
"I was trying to create a recipe for ... but since version x, the framework has deprecated the feature. I am not sure whether it will be useful to create a recipe anymore. What do you think?"
Of course, this is not the first time we have hesitated to create a recipe. While the recipe might be seen as providing redundant information, we believe it is valuable to create something that is applicable to a limited number of versions of the involved dependency. And therefore, we created the Library scope.
Library scope enables us to check whether a dependency is present in the project and conditionally apply Sensei recipes. This gives great flexibility as teams navigate through legacy code and dependencies.
Many of you might know of the Library scope that is already available under General Settings. So what is this you ask? We have enabled Library Scope to be present in YAML, just like the search. This creates a better user experience and does not break your flow as you create the recipe, which otherwise would have you navigate to General Settings and back to update metadata. More of these scopes will come into YAML but we have started with the Library Scope.
So let us dive a little further into how it works with an example.
Imagine the following case:
We want to create a Spring Web REST API that allows us to query some data. According to the best practices, we create a model for the entity we want to query. Next, we add a dependency to Hibernate ORM, so that we do not need to fiddle with the database structure. The ORM takes care of that for us. We also need to create a service that provides the data from the data access layer (CRUD repositories and so on) to the controller. Finally, we create the controller class for our API, where we will expose the allowed queries as endpoints.
To prevent having to expose different endpoints for each field that can be queried, we instead opt to use JPA 2 Specifications. To do so, we create a specification in the controller from the request URL. This specification describes what the entities we are looking for should look like. In the `Specification` class itself, we implement the `toPredicate` method to indicate how the specification can be validated.
But we are faced with an issue in the `toPredicate` method. To construct the predicate, we need to know the names of the columns in the database to compare. But because we are using an ORM, we do not have these columns present in a separate model. So, Hibernate's JPA 2 Metamodel Generator() comes to the rescue! This will help generate a metamodel for the entities we requested it to handle. These metamodels allow us to reference the column names as properties instead of hardcoding them.
Now that we have the metamodels generated, we want to put them to good use. Sensei can help anyone who works on the project by reminding them to use the metamodel, making sure that column names are not hardcoded anywhere. So let’s put that into practice.
To start, we take a look at the `toPredicate` method:
This basic predicate will only compare the name of our entity. We can expand on it using `and` clauses, but for the purpose of this recipe, this 'simple' check will do.
For the search component of the recipe, we want to call the method `get()` of the root parameter, so we select the `methodcall` option from the dropdown. Next, we want to limit the search to a methodcall with the name `get` whose signature is declared in the `Path` interface of the `javax.persistence.criteria` package. As the method has been overloaded, we also need to tell the search that our recipe only applies to the variant that takes a single string as an argument. To fix the issue of having the column names in the code we would want to use an argument of type `SingularAttribute` instead, the same type provided by the metamodel generator.
The recipe that we have created so far will trigger on any codebase that uses the JPA 2 `Path` interface, regardless of whether the codebase is set up to use the Hibernate Model Generator. If this library is present in the project, we want to indicate to the user that it should be used, so we add a library scope to the recipe.
And finally, our recipe is now ready.
With this recipe, any occurrence of `Path#get` that uses a string value as an argument will be flagged. As you can tell from the highlighted example code in the above screenshot, this recipe still works when the literal name of the column name is stored in an intermediate variable.
Note - We can also invert the library scope to handle the case where the library is not available, by prepending a `not` clause to the scope.
As we have seen in the example above, this new feature enables us to create more useful recipes by applying them based on the presence of dependencies in the project. To enhance its power even further, we included more options than were shown in the example, such as not only checking whether the dependency is present, but also applying conditions to the specific version of the dependency.
The main use cases we see for this feature are preventing recipes from providing duplicate information, detecting issues related to specific versions of dependencies, but also performing migrations from one dependency version to the next. We're looking forward to hearing from you about what uses you see for this feature.
As for all Sensei features, more information on the library scope is available in the reference documentation.