When using mybatis reverse engineering, the posture of delete method was not correct, which led to the table being emptied. After refreshing in production, we found that there was no data in the table, and a chill came from the foot center to the sky cover.

So I developed an interceptor and wrote this article to document and share.

I’m gonna have to do this on my own

Have you used Mybatis reverse engineering (Mybatis – generator-Maven-plugin) to generate relevant files?

Like this:


As you can see, reverse engineering helped us generate entity classes, Mapper interfaces, and mapper.xml.

It’s really easy to use. I’ve been using it for years, but it turned over some time ago.

What’s going on here? Let me show you.

Let’s start with the demand. In the process of an IOU data migration, all the IOU data and corresponding repayment plan data should be obtained through the interface of SERVICE A, and then these IOU data should be checked. If some additions cannot be satisfied, the IOU data and corresponding repayment plan should be deleted from the table.

The iOU and the corresponding repayment plan are stored in two tables, linked by the IOU number.

After I went online, I cleared the repayment schedule with laughter, but this inevitable problem had not been tested in the testing stage.


I immediately reached out to the DBA to help fix the data:


To simulate this scenario, I create two tables locally, the Order table (orderInfo) and the Order Extension table (orderInfoExt), which are associated by order numbers:

Just for demonstration purposes, so the two tables are very simple,


Let’s assume that the data of the order no. 2020060666666 in the table is judged to be wrong. The code I wrote at that time is reflected in the unit test like this:


See the problem?

The example object used in line 42 is the example of OrderInfo. The exampleExt object of the real OrderInfoExt object does not perform any assignment.

Why such an error?

Idea is too smart to blame! (Force an excuse)


I just need to dial ex and get back to…. Example appears in the code.

Mapper.xml does this by passing in an example with no arguments:


Run it and see what it looks like:


You see the delete from order_info_ext statement. Do you say you’re panicking?


Of course, the server on line cannot see the SQL being executed, but when the alarm message comes one after another, when the database is connected to look at the table and find that the data is missing.

Do you say you’re panicking?

Anyway, I found no data in the table after a refresh, a chill from the foot heart straight into the sky. This time is still a little panic, first shouted “Oh my God! Where’s the data?”


Then quickly report, ready to find the DBA data.

Fortunately, the error deletion does not affect normal services.

Without talking about the data recovery process, let’s talk about a few thoughts after this happened.

Oh, and why the test students didn’t find this problem. This is indeed a must-have question, and the test case is written about this test point.

But test students view the data with the SELECT statement, the query condition is really need to be deleted data.

Then execute the two tables separately and discover: the data is indeed gone.

Yes, the data is missing. The whole watch is clean.

What can I say when I see the test girl panicking?

This pot, don’t dump, I carry it on my own.


Reexamine reverse engineering

Let’s start with the interface that reverse engineering helped us generate:


I believe that mybatis reverse engineering friends, a look at the interface will know: yo, this is an old friend.

When I look at these interfaces again, I realize that there are still some problems.

For high-risk statements like DELETE, we still need to write XML by hand as much as possible.

For example, updateByExample also has a full table update because there is no WHERE condition.

For example, the select statement looks up the entire object, but sometimes we only need one value in the object.

For example, when a SELECT statement is used for large or critical table operations, there is no code constraint that SQL must be indexed.

How do we deal with all of these problems?

My advice is not to reverse engineer mybatis, write it all by hand.


Just kidding. We certainly can’t stop eating for fear of choking. Besides, reverse engineering does a lot of work for us, which is very convenient for CRUD boys like us.

So, I think the reverse engineering of Mybatis must have some configuration to control which interfaces are generated, don’t ask why, ask intuition.

If I were to develop such a plug-in, I would provide the switch configuration as well.

My current idea is not to let it generate delete related interface for me, I am afraid to use this interface.

So how do you configure it?

Let’s look in its DTD file:


It’s not a long file, it’s only 213 lines, and you can see this:


As you can see from your toes, this is the switch configuration we’re looking for. According to the description of the DTD file, these parameters are configured in the table tag.

Let’s try it:


And so it was. Then we carry out relevant configuration as follows:


Regenerate into:


Sure enough, the interface associated with delete is gone.

Then, when we really need the DELETE operation in our program, we will write the XML file ourselves.

So you forgot to write the WHERE condition in your own XML file so what do you do?

We don’t get paid this month. Reflect on yourself.


Of course, even if you do forget to write, the following interceptor can help you out.

Mybatis interceptor used

Actually, this was the first one THAT came to my mind. The cause of the above problem is simple: the delete statement is executed without a WHERE condition.

So we can intercept the SQL statement and make two judgments about it:

Whether it is a DELETE statement. If so, whether to include the WHERE condition.

So, how do we intercept this SQL?

The answer is that we can develop a Mybatis plugin, just like the paging plugin.

Plugins, as fancy as they sound, are actually interceptors. It’s very simple to implement.

Take a look at the official website:

English: https://mybatis.org/mybatis-3/zh/configuration.html#plugins

English: https://mybatis.org/mybatis-3/configuration.html


On the official website, the description of the plug-in module is as follows:

MyBatis provides a powerful mechanism, using the plug-in is very simple, just implement the Interceptor interface, and specify the method signature you want to intercept.

As the website says, plug-ins are very simple to develop and use. There are only three steps:

1. Implement the Interceptor interface.

2. Specify the method signature that you want to intercept.

3. Configure the plug-in.

Mybatis plug-in development

Based on the above three steps, we first take a look at how we write this plug-in, and the effect of this plug-in.

Mybatis version is 3.4.0.

The purpose of the interceptor in this article is to determine whether there is a WHERE condition in a DELETE statement. So, the resulting plug-in looks like this:


Here’s another copy and paste direct run version:

@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class}),
})
public class CheckSQLInterceptor implements Interceptor {
private static String SQL_WHERE = "where"; @override public Object Intercept (Invocation) throws Throwable {// Get the 0th argument of the method, that is, MappedStatement. MappedStatement MappedStatement = (MappedStatement) Invocation. GetArgs ()[0]; / / get the SQL command operation type SqlCommandType SqlCommandType = mappedStatement. GetSqlCommandType (); final Object[] queryArgs = invocation.getArgs(); final Object parameter = queryArgs[1]; BoundSql boundSql = mappedStatement.getBoundSql(parameter); String sql = boundSql.getSql(); If (SqlCommandType. DELETE. Equals (SqlCommandType)) {/ / formatting SQL SQL. SQL = replace (" \ n ", ""); if (! sql.toLowerCase().contains(SQL_WHERE)) { sql = sql.replace(" ", ""); Log.info (" delete statement no where condition, SQL :{}", SQL); Throw new Exception(" Delete statement without WHERE condition "); } } return invocation.proceed(); } @Override public Object plugin(Object o) { return Plugin.wrap(o, this); } @Override public void setProperties(Properties properties) { }Copy the code

Copy the code

}

Register the plugin (there are other ways to register the plugin, which will be covered later, but this is just to show how the Bean is injected) :


Let’s take a look at the implementation with the plug-in:


You can see the output in the log:

SQL :delete from order_info_ext

And throws an exception.

In this way, our extended table data is saved. In the beta phase, the beta testers are sure to be able to pull out the problem, just by glancing at the log.

Even if the test students forget the test, the production will not be successful. After the exception is thrown, an alarm message will be sent to the corresponding development person in charge, and the server will be uploaded to deal with it in time.

The functionality is implemented, and it’s really very simple.

Let’s get back to the code. Tell me: what was the most confusing part of the code when you got it?

The logic is simple. There’s nothing special about it. I think what most people get confused about when they get this code is this:


Why is the @intercepts Signature configured in this way?

Let’s take a look at the @intercepts annotation:


Inside is an array that can be configured with multiple signatures. So, in fact, this configuration is also possible:


The key is how @signature is configured:


We’ll talk about that in the next section.

The principle of mybatis plug-in

As we learned in the previous section, the difficulty in developing plug-ins is how @Signature is configured.

In fact, this can not be called difficult, can only say that you do not know what can be configured, more vacant just. This section answers that question.


To know how to configure mybatis, you must understand the four objects: Executor, ParameterHandler, ResultSetHandler, StatementHandler.

The website says:

MyBatis allows you to intercept calls at some point during the execution of a mapping statement. By default, MyBatis allows you to intercept method calls using plug-ins:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

ParameterHandler (getParameterObject, setParameters)

ResultSetHandler (handleResultSets, handleOutputParameters)

StatementHandler (prepare, parameterize, batch, update, query)

That official website said that these four objects are respectively used to do what?

Executor: The Executor of Mybatis, used for adding, deleting, modifying, or querying data.

ParameterHandler: ParameterHandler used to process parameter objects in SQL statements.

ResultSetHandler: Result handler that processes the returned results of SQL statements.

StatementHandler: Processing object of the database, used to execute SQL statements

Now that we know which of the four objects to intercept, we can take a look at the configuration of the above annotation:


The type field stores the class object, and its value range is the above mentioned four objects.

The Method field holds the specific method of the class object.

Args stores the parameters of a specific method.

What do you think of when you look at these parameters? Is there a conditioned thought reflex? If not, smack them again and see if you can get a little bit of reflection.


The purpose of the interceptor in this article is to determine if there is a WHERE condition in a DELETE statement, so after the analysis above, the Executor object will meet our requirements.

So the type field for @signature in this example is executor.class.

So what method do we put in the method field? Put the delete?

This depends on what methods an Executor object has:


As you can see, there are no DELETE methods, only query and update methods that are relevant to SQL execution.

However, we can take a wild guess: DELETE is also an update.

Then go and verify it:


You can see that the DELETE method does call the update method.

So in this case, the @signature method field puts the update method.


Args () {args () {args ();


Really, I think this is part of the hands-on-hands series. After this simple case, I hope you can do everything.

Let’s take a look at what pageHelper does.

As you can imagine with your toes, the paging plugin is definitely the way to block queries, we just need to verify that.

After introducing pageHelper, we can see that there are two more implementations of the Interceptor:


Let’s take a look at the PageInterceptor method:


Intercepting two query methods, one with four arguments and one with six:


Meanwhile, intercept’s implementation reads in part:


The four and six parameters are treated separately, and it’s a long story why they were treated this way, and why the two Query methods were intercepted.

Detailed can take a look at this link: https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md

Again, to write a good Mybatis plugin, you must know how to configure @signature. After the configuration can intercept what things, you should have points in mind.

The principle of mybatis plug-in

Now that we know how to write interceptors, let’s briefly analyze the principle of a wave.

A few days ago I saw a view that looked at the source code of the open source framework from Mybatis. I quite agree with the idea that it is really elegant and easy to understand. Many design patterns can be used.

To sum up the principle of mybatis plug-in is: dynamic proxy plus chain of responsibility.

Take a look at the dynamic proxy for the Plugin class:


The InvocationHandler is a JDK dynamic proxy.

The place labeled ② is the wrap method, which generates the Plugin proxy object.

The area marked ③ is the invoker method, circled in order to determine whether the current method needs to be blocked. If so, use proxy object to follow interceptor logic, if not, use target object to follow normal logic.

To show you the debug effect of this place:


A mediocre if judgment is the key to interceptors. Why does this place say more?

Because that’s where the details are. When the interviewer asks you how Mybatis determines whether it needs to intercept this method, you can answer it. Show you are really read the source code.

How does the chain of responsibility work?

Is this place: org. Apache. Ibatis. Plugin. InterceptorChain


You see another trick, Mybatis inside the design pattern and responsibility chain.

Let’s look at the caller to the pluginAll method:


This place shows what the website said earlier:

Plugins work with four objects: Executor, ParameterHandler, ResultSetHandler, and StatementHandler.


The four boxes above are where the plug-in is called.

So when is the plug-in loaded, or what is registered?

Back to the interceptor chain class:


We already know where the pluginAll method is called. There are actually two more questions in this method.

The first is the definition of the interceptor List, which uses the final modifier. So be aware of the difference between a final modified base type and a reference type. The contents of a reference type variable modified by a final can change.

The second is that getInterceptors return an unmodifiable List. Therefore, to modify the collection interceptors, you can only add elements through the addInterceptor method, which ensures that the collection is controllable.

So, we just need to know where the addInterceptor method is called and where the plug-in is registered.


One is SqlSessionFactoryBean and the other is XMLConfigBuilder.

The configuration with XML looks like this:


Familiar with Mybatis friends must know, nothing more than the analysis of tags.


After parsing to the plugins tag, you enter the pluginElement method, which calls addInterceptor:


This article does not use XML configuration, so let’s focus on SqlSessionFactoryBean.

What do you think?

Don’t blindly into the source code, add a breakpoint to see the call chain, follow the call chain is very clear.

Add a breakpoint at this point:


Then debug it so you can see the entire call chain:


Then we can find the source according to the above call chain:


The Interceptors are initialized in the constructor of MybatisAutoConfiguration.

And interceptorsProvider. GetIfAvailable () method also explains why we only need this into our interceptor in program can be found:


If you are not familiar with the getIfAvailable method, you can make up for it. I just want to show you the comment on the getIfAvailable method:


Of course, if you inject it this way, it may not work, and you will cry out, “What crap is written? It is configured incorrectly.”

Don’t worry. I’m not finished yet. See if there is a custom SqlSessionFactory in the project.

Have a look at the annotation above the source code that injected SqlSessionFactory?

ConditionalOnMissingBean (conditionalEssionFactory) ConditionalOnMissingBean (conditionalessionFactory) ConditionalOnMissingBean (conditionalessionFactory)


If you have a custom SqlSessionFactory, call factory.setplugins manually.

So, there are three ways to configure plug-ins:

1. Configure in XML mode.

2. If there is no custom SqlSessionFactory, inject @bean directly into the interceptor.

3. If you have a custom SqlSessionFactory, you need to manually call factory.setplugins in your custom location.

In fact, I tried the fourth method in the application. Properties configuration:


This configuration conforms to SpringBoot. It’s really silky, silky, silky.


Unfortunately, after I configure, click on the corresponding source code to have a look:


It calls the getInterceptors method, so I know something is wrong:

Sure enough, this error is reported when running: Failed to bind properties under ‘mybatis.configuration.interceptors’ to java.util.List

After searching for reasons, I finally found this issue:

github.com/mybatis/spring-boot-starter/issues/180

This “Doctor Strange” avatar user asked the same question I did:


And here’s the answer:


Don’t ask. Asking is not supporting. Please use @bean.

One last word (for attention)

Click a “like”, zhou is more tired, don’t whoring me, need some positive feedback.

If you find something wrong, please point it out so that I can correct it.

Thank you for reading, I insist on original, very welcome and thank you for your attention.

I am Why, a literary creator delayed by code. I am not a big shot, but I like sharing. I am a nice man in Sichuan who is warm and interesting.

Welcome to follow my wechat official account: WHY Technology. Here I’m going to share some knowledge about Java technology, being creative and responsible for every line of code. Occasionally also meeting shortage cavity go off board of chat about a life, write a book review, film review. Thank you for your attention, I wish you and I progress together.