Spring08_ Pure annotation practice_ Support transaction version

Keywords: Java SQL Apache Database

For the source code of this tutorial, please visit: tutorial_demo

In the previous tutorial, we used pure annotation and Apache Commons DbUtils to implement CRUD operation of single table, but the operation in this tutorial does not support transaction. In this tutorial, we changed it to transaction support version according to the existing knowledge, so as to prepare for the follow-up study.

1, Analysis of transfer operation problems

Next, we implement a transfer operation to analyze the existing problems.

1.1. Add corresponding methods to IAccountService

//New transfer method
void transfer(Integer srcId, Integer dstId, Float money);

1.2. Implement the newly added method in the business layer implementation class AccountServiceImpl

//Transfer operation
@Override
public void transfer(Integer srcId, Integer dstId, Float money) {
    //Query users who need to transfer according to Id
	Account src = accountDao.findById(srcId);
    Account dst = accountDao.findById(dstId);
        
	if(src == null) {
		throw new RuntimeException("Transfer out user does not exist");
	}
        
	if(dst == null) {
		throw new RuntimeException("Transfer in user does not exist");
	}
        
	if(src.getMoney() < money) {
		throw new RuntimeException("Insufficient balance of transfer out account");
    }
     
    //One decrease, one increase
	src.setMoney(src.getMoney() - money);
	dst.setMoney(dst.getMoney() + money);
        
    //Update in database
    accountDao.update(src);
    accountDao.update(dst);
}

1.3 add test method to test class

@Test
public void testTrans() {
	accountService.transfer(1, 2, 10F);
}

Run test method, test successful. There seems to be no problem.

1.4 modification of transfer method at Service level

//Transfer operation
@Override
public void transfer(Integer srcId, Integer dstId, Float money) {
    //Query users who need to transfer according to Id
	Account src = accountDao.findById(srcId);
    Account dst = accountDao.findById(dstId);
        
	if(src == null) {
		throw new RuntimeException("Transfer out user does not exist");
	}
        
	if(dst == null) {
		throw new RuntimeException("Transfer in user does not exist");
	}
        
	if(src.getMoney() < money) {
		throw new RuntimeException("Insufficient balance of transfer out account");
    }
     
    //One decrease, one increase
	src.setMoney(src.getMoney() - money);
	dst.setMoney(dst.getMoney() + money);
        
    //Update in database
    accountDao.update(src);
    
    //The newly added content makes an exception artificially
    int i = 100/0;
    
    //Update in database
    accountDao.update(dst);
}

Running the test method, the program is abnormal, but src has less money in the database.

Transfer process analysis:

  1. The principle of large transfer process is to reduce the source account money and increase the target account money;
  2. The operation of reducing the source account money and increasing the target account money either succeeds at the same time or fails at the same time;
  3. In order to achieve simultaneous success, related operations should be wrapped in the same transaction in case of simultaneous failure;
  4. The same Connection should be used for the same transaction operation.

Current problems: no transaction is opened in the current transfer process, and all operations use independent connections.

2, Code upgrade

According to the previous analysis, we need to modify the project to support transactions. The following principles should be met:

  1. Transaction processing in the Service layer (start transaction, commit, rollback);
  2. Dao layer is only responsible for database operation and not for business, that is to say, each Dao operation only performs the most fine-grained CRUD operation;
  3. The Dao layer does not handle exceptions, and the exceptions are thrown to the Service layer.

2.1. Create a tool class to support transactions

package org.codeaction.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@Component
public class JdbcUtils {
    private static DataSource dataSource;
    //Used to save the connection of the current thread
    private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
    //Get connection pool object
    public static DataSource getDataSource() {
          return dataSource;
    }

    //Pay attention to this position
	@Autowired
	public void setDataSource(DataSource dataSource) {
		JdbcUtils.dataSource = dataSource;
	}

	//Get connection
    public static Connection getConnection() throws SQLException {
		System.out.println(dataSource);
    	Connection conn = tl.get();
    	if(conn == null) {
    		return dataSource.getConnection();
    	}
    	return conn;
    }
    //Open transaction
    public static void beginTransaction() throws SQLException {
    	Connection conn = tl.get();
		if(conn != null) {
			throw new SQLException("Transaction has been opened, cannot be opened repeatedly");
		}
		conn = getConnection();
		conn.setAutoCommit(false);
		tl.set(conn);
	}
    //Commit transaction
    public static void commitTransaction() throws SQLException {
    	Connection conn = tl.get();
		if(conn == null) {
			throw new SQLException("Connection is empty, cannot commit transaction");
		}
		conn.commit();
		conn.close();
		tl.remove();
	}
    //Rollback transaction
    public static void rollbackTransaction() throws SQLException {
		Connection conn = tl.get();
		if (conn == null) {
			throw new SQLException("Connection is empty, transaction cannot be rolled back");
		}
		conn.rollback();
		conn.close();
		tl.remove();
	}
}

explain:

  1. This project uses ThreadLocal to ensure that it can be used in multi-threaded environment;
  2. Instead of using @ AutoWired on it, the dataSource property uses @ AutoWired on its set method. The reason is that dataSource is of static type, static type variable creation is earlier than Spring container creation, and Spring context has not been loaded when class loader loads static variables. So the classloader will not inject the static class correctly into the bean, and it will fail.

2.2. Modify the implementation class of Dao

package org.codeaction.dao.impl;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.codeaction.dao.IAccountDao;
import org.codeaction.domain.Account;
import org.codeaction.util.JdbcUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner queryRunner;

    public void setQueryRunner(QueryRunner queryRunner) {
        this.queryRunner = queryRunner;
    }

    @Override
    public List<Account> findAll() throws SQLException {
        Connection conn = JdbcUtils.getConnection();
        String sql = "select * from account";
        List<Account> list = queryRunner.query(conn, sql, new BeanListHandler<Account>(Account.class));
        return list;
    }

    @Override
    public Account findById(Integer id) throws SQLException {
        Connection conn = JdbcUtils.getConnection();
        String sql = "select * from account where id = ?";
        Account account = queryRunner.query(conn, sql, new BeanHandler<Account>(Account.class), id);
        return account;
    }

    @Override
    public void save(Account account) throws SQLException {
        Object[] params = {account.getName(), account.getMoney()};
        Connection conn = JdbcUtils.getConnection();
        String sql = "insert into account(name, money) values(?, ?)";
        queryRunner.update(conn, sql, params);
    }

    @Override
    public void update(Account account) throws SQLException {
        Object[] params = {account.getName(), account.getMoney(), account.getId()};
        Connection conn = JdbcUtils.getConnection();
        String sql = "update account set name=?, money=? where id=?";
        queryRunner.update(conn, sql, params);
    }

    @Override
    public void delete(Integer id) throws SQLException {
        Object[] params = {id};
        Connection conn = JdbcUtils.getConnection();
        String sql = "delete from account where id=?";
        queryRunner.update(conn, sql, id);
    }
}

2.3 modify the Service layer implementation class

package org.codeaction.service.impl;

import org.codeaction.dao.IAccountDao;
import org.codeaction.domain.Account;
import org.codeaction.service.IAccountService;
import org.codeaction.util.JdbcUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.sql.SQLException;
import java.util.List;

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public List<Account> findAll() {
        List<Account> list = null;

        try {
            //1. Open transaction
            JdbcUtils.beginTransaction();
            //2. Operation
            list = accountDao.findAll();
            //3. Submission
            JdbcUtils.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //Rollback transaction
                JdbcUtils.rollbackTransaction();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

        return list;
    }

    @Override
    public Account findById(Integer id) {
        Account account = null;

        try {
            //1. Open transaction
            JdbcUtils.beginTransaction();
            //2. Operation
            account = accountDao.findById(id);
            //3. Submission
            JdbcUtils.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //Rollback transaction
                JdbcUtils.rollbackTransaction();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        return account;
    }

    @Override
    public void save(Account account) {
        try {
            //1. Open transaction
            JdbcUtils.beginTransaction();
            //2. Operation
            accountDao.save(account);
            //3. Submission
            JdbcUtils.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //Rollback transaction
                JdbcUtils.rollbackTransaction();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
    }

    @Override
    public void update(Account account) {
        try {
            //1. Open transaction
            JdbcUtils.beginTransaction();
            //2. Operation
            accountDao.update(account);
            //3. Submission
            JdbcUtils.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //Rollback transaction
                JdbcUtils.rollbackTransaction();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }

    @Override
    public void delete(Integer id) {
        try {
            //1. Open transaction
            JdbcUtils.beginTransaction();
            //2. Operation
            accountDao.delete(id);
            //3. Submission
            JdbcUtils.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //Rollback transaction
                JdbcUtils.rollbackTransaction();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
    }

    @Override
    public void transfer(Integer srcId, Integer dstId, Float money) {
        try {
            //1. Open transaction
            JdbcUtils.beginTransaction();
            //2. Operation
            Account src = accountDao.findById(srcId);
            Account dst = accountDao.findById(dstId);

            if(src == null) {
                throw new RuntimeException("Transfer out user does not exist");
            }

            if(dst == null) {
                throw new RuntimeException("Transfer in user does not exist");
            }

            if(src.getMoney() < money) {
                throw new RuntimeException("Insufficient balance of transfer out account");
            }

            src.setMoney(src.getMoney() - money);
            dst.setMoney(dst.getMoney() + money);

            accountDao.update(src);

            //int x = 1/0; / / note here

            accountDao.update(dst);
            //3. Submission
            JdbcUtils.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                //Rollback transaction
                JdbcUtils.rollbackTransaction();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
    }
}

2.4. Modify the JdbcConfig configuration class

package org.codeaction.config;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.codeaction.util.JdbcUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class JdbcConfig {
    @Value("${jdbc.driverClass}")
    private String driverClass;
    @Value("${jdbc.jdbcUrl}")
    private String jdbcUrl;
    @Value("${jdbc.user}")
    private String user;
    @Value("${jdbc.password}")
    private String password;


    @Bean("queryRunner")
    public QueryRunner queryRunner() {
        return new QueryRunner();
    }

    @Bean("dataSource")
    public DataSource dataSource() {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        try {
            dataSource.setDriverClass(driverClass);
            dataSource.setJdbcUrl(jdbcUrl);
            dataSource.setUser(user);
            dataSource.setPassword(password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }
}

2.5 operation test method

@Test
public void testTrans() {
	accountService.transfer(1, 2, 10F);
}

The test method can be run successfully;

Cancel the comment of int i = 1/0; in 2.4, run the test method, and if there is an exception, it can roll back normally.

2.6 current code problems

The current code has been able to support transactions, but there are still some problems, as follows:

  1. The code is too cumbersome. Compared with the previous version, the Service layer implementation class is too cumbersome. It is not only responsible for business but also responsible for transaction operations;
  2. If there is a change in the method related to transaction operation in the JdbcUtils class, the corresponding call location of the Service layer code will be changed. If the project is very large, it is a nightmare for developers.

In the following sections, we will learn more about Spring AOP to solve the above problems.

Posted by 3dron on Fri, 29 May 2020 04:21:38 -0700