001/*
002 *  Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.transaction;
017
018import org.apache.ibatis.logging.Log;
019import org.apache.ibatis.logging.LogFactory;
020
021import java.sql.Connection;
022import java.sql.SQLException;
023import java.util.Map;
024import java.util.UUID;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.function.Supplier;
027
028/**
029 * 事务管理器
030 */
031public class TransactionalManager {
032
033    private TransactionalManager() {
034    }
035
036    private static final Log log = LogFactory.getLog(TransactionalManager.class);
037
038    //<xid : <dataSourceKey : connection>>
039    private static final ThreadLocal<Map<String, Map<String, Connection>>> CONNECTION_HOLDER
040        = ThreadLocal.withInitial(ConcurrentHashMap::new);
041
042
043    public static void hold(String xid, String ds, Connection connection) {
044        Map<String, Map<String, Connection>> holdMap = CONNECTION_HOLDER.get();
045        Map<String, Connection> connMap = holdMap.get(xid);
046        if (connMap == null) {
047            connMap = new ConcurrentHashMap<>();
048            holdMap.put(xid, connMap);
049        }
050
051        if (connMap.containsKey(ds)) {
052            return;
053        }
054
055        connMap.put(ds, connection);
056    }
057
058
059    public static <T> T exec(Supplier<T> supplier, Propagation propagation, boolean withResult) {
060        //上一级事务的id,支持事务嵌套
061        String currentXID = TransactionContext.getXID();
062        try {
063            switch (propagation) {
064                //若存在当前事务,则加入当前事务,若不存在当前事务,则创建新的事务
065                case REQUIRED:
066                    if (currentXID != null) {
067                        return supplier.get();
068                    } else {
069                        return execNewTransactional(supplier, withResult);
070                    }
071
072
073                //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
074                case SUPPORTS:
075                    return supplier.get();
076
077
078                //若存在当前事务,则加入当前事务,若不存在当前事务,则已非事务的方式运行
079                case MANDATORY:
080                    if (currentXID != null) {
081                        return supplier.get();
082                    } else {
083                        throw new TransactionException("No existing transaction found for transaction marked with propagation 'mandatory'");
084                    }
085
086
087                //始终以新事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。
088                case REQUIRES_NEW:
089                    return execNewTransactional(supplier, withResult);
090
091
092                //以非事务的方式运行,若存在当前事务,则暂停(挂起)当前事务。
093                case NOT_SUPPORTED:
094                    if (currentXID != null) {
095                        TransactionContext.release();
096                    }
097                    return supplier.get();
098
099
100                //以非事务的方式运行,若存在当前事务,则抛出异常。
101                case NEVER:
102                    if (currentXID != null) {
103                        throw new TransactionException("Existing transaction found for transaction marked with propagation 'never'");
104                    }
105                    return supplier.get();
106
107
108                //暂时不支持这种事务传递方式
109                //default 为 nested 方式
110                default:
111                    throw new TransactionException("Transaction manager does not allow nested transactions");
112
113            }
114        } finally {
115            //恢复上一级事务
116            if (currentXID != null) {
117                TransactionContext.holdXID(currentXID);
118            }
119        }
120    }
121
122    private static <T> T execNewTransactional(Supplier<T> supplier, boolean withResult) {
123        String xid = startTransactional();
124        T result = null;
125        boolean isRollback = false;
126        try {
127            result = supplier.get();
128        } catch (Throwable e) {
129            isRollback = true;
130            rollback(xid);
131            throw new TransactionException(e.getMessage(), e);
132        } finally {
133            if (!isRollback) {
134                if (!withResult) {
135                    if (result instanceof Boolean && (Boolean) result) {
136                        commit(xid);
137                    }
138                    //null or false
139                    else {
140                        rollback(xid);
141                    }
142                } else {
143                    commit(xid);
144                }
145            }
146        }
147        return result;
148    }
149
150
151    public static Connection getConnection(String xid, String ds) {
152        Map<String, Connection> connections = CONNECTION_HOLDER.get().get(xid);
153        return connections == null || connections.isEmpty() ? null : connections.get(ds);
154    }
155
156
157    public static String startTransactional() {
158        String xid = UUID.randomUUID().toString();
159        TransactionContext.holdXID(xid);
160        return xid;
161    }
162
163    public static void commit(String xid) {
164        release(xid, true);
165    }
166
167    public static void rollback(String xid) {
168        release(xid, false);
169    }
170
171    private static void release(String xid, boolean commit) {
172        //先release,才能正常的进行 commit 或者 rollback.
173        TransactionContext.release();
174
175        Exception exception = null;
176        Map<String, Map<String, Connection>> holdMap = CONNECTION_HOLDER.get();
177        try {
178            if (holdMap.isEmpty()) {
179                return;
180            }
181            Map<String, Connection> connections = holdMap.get(xid);
182            if (connections != null) {
183                for (Connection conn : connections.values()) {
184                    try {
185                        if (commit) {
186                            conn.commit();
187                        } else {
188                            conn.rollback();
189                        }
190                    } catch (SQLException e) {
191                        exception = e;
192                    } finally {
193                        try {
194                            conn.close();
195                        } catch (SQLException e) {
196                            //ignore
197                        }
198                    }
199                }
200            }
201        } finally {
202            holdMap.remove(xid);
203
204            if (holdMap.isEmpty()) {
205                CONNECTION_HOLDER.remove();
206            }
207            if (exception != null) {
208                log.error("TransactionalManager.release() is error. Cause: " + exception.getMessage(), exception);
209            }
210        }
211    }
212
213}