Using MyBatis in WEB applications (using the MVC architecture pattern)

2023.10.30

This chapter will use MyBatis in a web application to implement a bank transfer function. The overall architecture adopts the MVC architecture pattern.

Initialization of database tables

Initial configuration of the environment

Configuration of web.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0"
         metadata-complete="false">
    
<!-- <servlet>-->
<!-- <servlet-name>test</servlet-name>-->
<!-- <servlet-class>bank.web.AccountServlet</servlet-class>-->
<!-- </servlet>-->
<!-- <servlet-mapping>-->
<!-- <servlet-name>test</servlet-name>-->
<!-- <url-pattern>/transfer</url-pattern>-->
<!-- </servlet-mapping>-->

</web-app>

ps: If metadata-complete is filled in with true, the annotations cannot be used. The path must be configured in the xml file, so if you fill in false here, the annotations can be used.

Pom.xml file configuration:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>jay</groupId>
  <artifactId>mybatis-004-web</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>mybatis-004-web Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.13</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.11</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-servlet-api</artifactId>
      <version>10.0.12</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>mybatis-004-web</finalName>
  </build>
</project>

Introduce relevant configuration files and place them in the resources directory:

AccountMapper.xml file: configure related sql statements.

logback-test.xml file

mybatis-config.xml file:

Front-end page preparation

index.html: Starting the server will automatically jump to this page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Bank account transfer</title>
</head>
<body>
<!--/bank is the root of the application. You must pay attention to this name when deploying web applications to tomcat -->
<form action="/bank/transfer" method="post">
    Transfer out account: <input type="text" name="fromActno"/><br>
    Transfer account: <input type="text" name="toActno"/><br>
    Transfer amount: <input type="text" name="money"/><br>
    <input type="submit" value="Transfer"/>
</form>
</body>
</html>

error1.html: When the transfer fails due to “insufficient balance”, it will jump to this page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Transfer Report</title>
</head>
<body>
<h1>Insufficient balance! ! ! ! </h1>
</body>
</html>

error.html: When the transfer fails due to unknown reasons, it will jump to this page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Transfer Report</title>
</head>
<body>
<h1>Transfer failed, unknown reason! ! ! </h1>
</body>
</html>

success.html: Jump to this page if the transfer is successful

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Transfer Report</title>
</head>
<body>
<h1>Transfer successful! ! ! </h1>
</body>
</html>

Create pojo package, service package, dao package, web package, utils, exceptions package under bank package:

dao is the data access layer, also known as the persistence layer. It is located at the bottom of the three layers and is used to process data.

Service is a business logic layer used to encapsulate business logic.

The web is the presentation layer used to display data and receive data input by users, providing users with an interactive interface.

Pojo is an entity class, used to encapsulate data.

utils is a tool class.

Exceptions are exception handling.

Define pojo class: Account

package bank.pojo;

public class Account {
    private Long id;
    private String actno;
    private Double balance;

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", actno='" + actno + ''' +
                ", balance=" + balance +
                '}';
    }

    public Account() {
    }

    public Account(Long id, String actno, Double balance) {
        this.id = id;
        this.actno = actno;
        this.balance = balance;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }
}

Write the AccountDao interface and AccountDaoImpl implementation class

package bank.dao;

import bank.pojo.Account;

public interface AccountDao {

    /**
     * Get account information based on account number
     * @param actno account
     * @return account information
     */
    Account selectByActno(String actno);

    /**
     * Update account information
     * @param act account information
     * @return 1 indicates successful update, other values indicate failure
     */
    int update(Account act);
}
package bank.dao.impl;


import bank.dao.AccountDao;
import org.apache.ibatis.session.SqlSession;
import bank.pojo.Account;
import bank.utils.SqlSessionUtil;

import javax.swing.plaf.IconUIResource;

public class AccountDaoImpl implements AccountDao {
    public Account selectByActno(String actno) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        Account act = (Account) sqlSession.selectOne("selectByActno",actno);

        return act;
    }

    public int update(Account act) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        int count = sqlSession.update("update",act);

        return count;
    }
}

Write AccountService interface and AccountServiceImpl implementation class

package bank.service;

import bank.exceptions.MoneyNotEnoughException;
import bank.exceptions.TransferException;

public interface AccountService {
    void transfer(String fromActno,String toActno,double money) throws MoneyNotEnoughException, TransferException;
}
package bank.service.impl;

import bank.dao.AccountDao;
import bank.dao.impl.AccountDaoImpl;
import bank.exceptions.MoneyNotEnoughException;
import bank.exceptions.TransferException;
import bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import bank.pojo.Account;
import bank.service.AccountService;



public class AccountServiceImpl implements AccountService {
    private AccountDao accountDao = new AccountDaoImpl();
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
        //Add transaction control code
        SqlSession sqlSession = SqlSessionUtil.openSession();


        //Query the account balance to determine whether the balance of the transferred account is sufficient
        Account fromAct = accountDao.selectByActno(fromActno);
        if(fromAct.getBalance() < money){
            throw new MoneyNotEnoughException("Sorry, insufficient balance!");
        }

        //Execution to this point indicates that the balance is sufficient
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance()-money);
        toAct.setBalance(toAct.getBalance() + money);
        //update database
        int count = accountDao.update(fromAct);

        count + = accountDao.update(toAct);
        if(count != 2){
            throw new TransferException("Transfer exception, unknown reason");
        }

        sqlSession.commit();
        SqlSessionUtil.close(sqlSession);
    }
}

Exception class

package bank.exceptions;

public class MoneyNotEnoughException extends Exception{
    public MoneyNotEnoughException(){}
    public MoneyNotEnoughException(String msg){
        super(msg);
    }
}
package bank.exceptions;

public class TransferException extends Exception{
    public TransferException(){}
    public TransferException(String msg){

    }
}

Tool class SqlSessionUtil:

package bank.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;

public class SqlSessionUtil {
    private SqlSessionUtil(){};

    private static SqlSessionFactory sqlSessionFactory;

    //Static code block: run when the class is loaded
    static {
        try {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //Global, server level, just define one in a server.
    private static ThreadLocal<SqlSession> local = new ThreadLocal<SqlSession>();

    //Get session object
    public static SqlSession openSession(){
        SqlSession sqlSession = local.get();
        if(sqlSession == null){
            sqlSession = sqlSessionFactory.openSession();
            //Bind the sqlSession object to the current thread
            local.set(sqlSession);
        }
        return sqlSession;
    }

    //Remove the SqlSession object from the current thread
    public static void close(SqlSession sqlSession){
        if(sqlSession != null){
            sqlSession.close();
            //Pay attention to removing the binding relationship between the SqlSession object and the current thread
            //Because Tomcat server supports thread pool, which means that used thread objects may be used again next time.
            local.remove();
        }
    }

}

Presentation layerAccountServlet class:

package bank.web;

import bank.exceptions.MoneyNotEnoughException;
import bank.exceptions.TransferException;
import bank.service.AccountService;
import bank.service.impl.AccountServiceImpl;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
    private AccountService accountService = new AccountServiceImpl();
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //Get form data
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));
        //Call the transfer method of service to complete the transfer (adjust the business layer)
        try {
            accountService.transfer(fromActno,toActno,money);
            response.sendRedirect(request.getContextPath() + "/success.html");
        } catch (MoneyNotEnoughException e) {
            response.sendRedirect(request.getContextPath() + "/error1.html");
        } catch (TransferException e) {
            response.sendRedirect(request.getContextPath() + "/error2.html");
        }
    }
}

Experimental results:

Start the server and the page appears:

Enter the amount:

Jump page:

Data in the database:

Transaction issues

In the AccountServiceImpl implementation class, transaction statements are added at the end to ensure transaction security. In order to ensure that the SqlSession object used in service and dao is the same, the SqlSession object can be stored in ThreadLocal. Therefore, in the tool class SqlSessionUtil, a global ThreadLocal is used. Its usage is mainly to solve the problem of inconsistency due to data concurrency in multi-threads. The code to obtain the session object is:

 public static SqlSession openSession(){
        SqlSession sqlSession = local.get();
        if(sqlSession == null){
            sqlSession = sqlSessionFactory.openSession();
            //Bind the sqlSession object to the current thread
            local.set(sqlSession);
        }
        return sqlSession;
    }

That is, one thread corresponds to one sqlSession. In this way, the sqlsession in dao and service will be the same.

Note here that local needs to be removed in the close method of sqlsession:

 public static void close(SqlSession sqlSession){
        if(sqlSession != null){
            sqlSession.close();
            //Pay attention to removing the binding relationship between the SqlSession object and the current thread
            //Because Tomcat server supports thread pool, which means that used thread objects may be used again next time.
            local.remove();
        }
    }

MyBatis’s three object scopes

SqlSessionFactoryBuilder:

This class can be instantiated, used, and discarded; once the SqlSessionFactory is created, it is no longer needed. Therefore the best scope for a SqlSessionFactoryBuilder instance is method scope (that is, local method variables). You can reuse SqlSessionFactoryBuilder to create multiple SqlSessionFactory instances, but it’s best not to keep it around all the time to ensure that all XML parsing resources can be released for more important things.

SqlSessionFactory:

Once created, the SqlSessionFactory should persist for the duration of the application, without any reason to discard it or recreate another instance. The best practice for using SqlSessionFactory is not to re-create it multiple times during the application runtime. Rebuilding SqlSessionFactory multiple times is considered a “bad habit” in coding. Therefore, the best scope for SqlSessionFactory is to use the MyBatis application scope in a WEB application. There are many ways to do this, the simplest is to use the singleton pattern or the static singleton pattern.

SqlSession:

Each thread should have its own SqlSession instance. Instances of SqlSession are not thread-safe and therefore cannot be shared, so their best scope is the request or method scope. You must not place a reference to a SqlSession instance in a static field of a class, or even in an instance variable of a class. You should also never place a reference to a SqlSession instance in any kind of managed scope, such as HttpSession in the Servlet framework. If you are currently using a web framework, consider placing the SqlSession in a scope similar to the HTTP request. In other words, every time an HTTP request is received, a SqlSession can be opened, and after a response is returned, it can be closed. This shutdown operation is very important. In order to ensure that the shutdown operation can be performed every time, you should put this shutdown operation in a finally block.