Mysql--实战篇--@Transactional失效场景及避免策略(@Transactional实现原理,失效场景,内部调用问题等)

在Spring框架中,@Transactional注解用于声明式事务管理,能够简化事务的处理逻辑。然而,在某些情况下,@Transactional可能会失效,导致事务无法按预期工作。了解这些失效场景及其原因,可以帮助你更好地管理和调试事务问题。

1、@Transactional失效的常见场景

(1)、方法非public访问权限

@Transactional注解通常只能应用于public方法上。如果将其应用于protected、private或包级私有方法上,由于Spring的代理机制无法拦截这些方法的调用,因此事务注解将失效。

(2)、同一个类的内部调用

当@Transactional注解的方法在同一类内部被另一个方法调用时,事务可能会失效。这是因为Spring的AOP(面向切面编程)机制是通过代理对象来实现事务管理的。只有当外部类通过代理对象调用带有@Transactional注解的方法时,Spring才会拦截该方法并为其创建事务。而在同一类内部直接调用方法时,不会经过代理对象,因此事务不会生效。

内部调用示例:

@Service
public class UserService {@Autowiredprivate UserRepository userRepository;// 带有@Transactional注解的方法@Transactionalpublic void createUser(User user) {userRepository.save(user);}// 同一类内部调用带有@Transactional注解的方法public void createUserInternal() {User user = new User();user.setName("Alice");createUser(user);      // 事务不会生效}
}

解释:
在上面的例子中,createUser方法虽然带有@Transactional注解,但在createUserInternal方法中直接调用了createUser,这不会触发Spring的事务管理机制。因为createUserInternal和createUser是同一个类的方法,调用是通过this引用进行的,而不是通过代理对象调用的,因此事务不会生效。

解决方案:

  • 拆分到不同类:将createUser方法移到另一个服务类中,确保它是通过代理对象调用的。
    示例:
  @Servicepublic class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser(User user) {userRepository.save(user);}}@Servicepublic class AnotherService {@Autowiredprivate UserService userService;public void createUserInternal() {User user = new User();user.setName("Alice");userService.createUser(user);  // 通过代理对象调用,事务生效}}
  • 使用自定义代理:可以通过AopContext.currentProxy()获取当前类的代理对象,然后通过代理对象调用方法。
    示例:
  @Servicepublic class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser(User user) {userRepository.save(user);}public void createUserInternal() {User user = new User();user.setName("Alice");((UserService) AopContext.currentProxy()).createUser(user);  // 使用代理对象调用}}

(3)、事务管理器配置错误

如果Spring容器中配置了多个事务管理器,但在使用@Transactional注解时没有明确指定事务管理器,可能会导致Spring使用默认的事务管理器,而这个默认的事务管理器可能不适用于当前的操作,从而导致事务注解失效。

(4)、方法内部捕捉异常

在使用@Transactional注解的方法中,如果内部捕获了可能导致事务回滚的异常,并且没有重新抛出一个Spring框架能够识别的运行时异常或声明式异常,那么事务管理器将无法感知到异常,从而可能导致事务不会回滚。
@Transactional注解默认只会对RuntimeException和未检查的异常进行回滚。如果在事务方法中捕获了异常并吞掉(即没有抛出或记录),事务将不会回滚,导致数据不一致。

错误示例:

@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser(User user) {try {userRepository.save(user);// 模拟异常if (user.getName().equals("Alice")) {throw new RuntimeException("模拟异常");}} catch (Exception e) {// 异常被捕获并吞掉,事务不会回滚System.out.println("捕获到异常:" + e.getMessage());}}
}

解释:
在上面的例子中,当user.getName()等于"Alice"时,会抛出一个RuntimeException,但这个异常被捕获并在catch块中处理,没有重新抛出。由于@Transactional注解默认只对未捕获的RuntimeException进行回滚,因此事务不会回滚,导致数据不一致。

解决方案:

  • 不要吞掉异常:确保在catch块中记录异常日志,并根据需要重新抛出异常。
    示例:
  @Transactionalpublic void createUser(User user) {try {userRepository.save(user);if (user.getName().equals("Alice")) {throw new RuntimeException("模拟异常");}} catch (Exception e) {// 记录异常日志logger.error("创建用户时发生异常", e);// 重新抛出异常,确保事务回滚throw e;}}
  • 手动标记事务为回滚:如果不希望重新抛出异常,可以使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记事务为回滚。
    示例:
  import org.springframework.transaction.interceptor.TransactionAspectSupport;@Transactionalpublic void createUser(User user) {try {userRepository.save(user);if (user.getName().equals("Alice")) {throw new RuntimeException("模拟异常");}} catch (Exception e) {// 手动标记事务为回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();logger.error("创建用户时发生异常", e);}}

(5)、使用final修饰的方法

如果使用final关键字修饰了方法,那么由于该方法不能被重写,Spring的代理机制将无法对其应用@Transactional注解,因此事务将失效。

(6)、静态方法

静态方法同样无法通过动态代理来应用@Transactional注解,因为静态方法不属于类的实例方法,而是属于类本身。

(7)、未被Spring管理的类

如果一个类没有被Spring管理(即没有使用@Controller、@Service、@Component、@Repository等注解进行标注),那么该类中的方法即使使用了@Transactional注解也不会生效。

(8)、传播特性配置错误

@Transactional 注解支持多种事务传播行为(Propagation),不同的传播行为会影响事务的创建和管理方式。如果不正确配置传播行为,可能会导致事务不按预期工作。
可以指定propagation参数来定义事务的传播行为。如果传播特性配置错误(例如设置为Propagation.NEVER,而当前存在事务),则事务将不会生效。

常见的传播行为:

  • REQUIRED(默认):如果当前存在事务,则加入该事务;否则创建一个新的事务。
  • REQUIRES_NEW:总是创建一个新的事务,如果当前存在事务,则将其挂起。
  • SUPPORTS:如果当前存在事务,则加入该事务;否则以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将其挂起。
  • MANDATORY:必须在一个现有的事务中执行,否则抛出异常。
  • NEVER:必须在没有事务的情况下执行,否则抛出异常。
  • NESTED:如果当前存在事务,则在嵌套事务内执行;否则创建一个新的事务。

示例:

@Service
public class UserService {@Transactional(propagation = Propagation.REQUIRED)public void methodA() {// 执行一些数据库操作methodB();}@Transactional(propagation = Propagation.NOT_SUPPORTED)public void methodB() {// 这里的事务不会生效,因为propagation设置为NOT_SUPPORTED// 任何数据库操作都不会参与事务}
}

解决方案:

  • 根据业务需求选择合适的传播行为。例如,如果你希望methodB与methodA共享同一个事务,应该使用REQUIRED或REQUIRES_NEW,而不是NOT_SUPPORTED或NEVER。
    改进后的代码:
  @Servicepublic class UserService {@Transactional(propagation = Propagation.REQUIRED)public void methodA() {// 执行一些数据库操作methodB();}@Transactional(propagation = Propagation.REQUIRED)public void methodB() {// 现在 methodB 会与 methodA 共享同一个事务}}

(9)、数据库不支持事务

如果使用的数据库表不支持事务(例如,某些类型的存储引擎或数据库系统不支持事务),那么即使使用了@Transactional注解,事务也不会生效。

(10)、异步线程调用

在Spring中,@Async注解用于开启异步任务执行。默认情况下,@Async和@Transactional不能同时生效。这是因为在异步任务中,Spring的事务管理器无法正确管理事务,导致事务失效。

异常示例:

@Service
public class AsyncService {@Autowiredprivate UserRepository userRepository;@Async@Transactionalpublic void createUserAsync(User user) {userRepository.save(user);// 模拟异常if (user.getName().equals("Alice")) {throw new RuntimeException("模拟异常");}}
}

解释:
在上面的例子中,createUserAsync方法同时标注了@Async和@Transactional。由于@Async注解会将方法的执行交给一个新的线程池,而Spring的事务管理器是基于主线程的,因此在异步任务中,事务管理器无法正确管理事务,导致事务失效。

解决方案:

  • 使用Propagation.REQUIRES_NEW:可以在异步方法中使用Propagation.REQUIRES_NEW来强制创建一个新的事务。这样即使在异步任务中,事务也能正常工作。
    示例:
  @Async@Transactional(propagation = Propagation.REQUIRES_NEW)  // 强制执行事务public void createUserAsync(User user) {userRepository.save(user);if (user.getName().equals("Alice")) {throw new RuntimeException("模拟异常");}}
  • 避免在异步方法中使用事务:如果异步任务不需要事务支持,建议将事务逻辑移到同步方法中,或者在异步任务中手动管理事务。

(11)、事务回滚规则设置不当

@Transactional注解允许你指定哪些异常会导致事务回滚(rollbackFor)和哪些异常不会导致事务回滚(noRollbackFor)。如果不正确配置这些规则,可能会导致事务在不应该回滚的情况下回滚,或者在应该回滚的情况下没有回滚。

示例:

@Service
public class UserService {@Transactional(rollbackFor = Exception.class)public void updateUser(User user) {// 执行更新操作userMapper.updateUser(user);// 抛出一个自定义异常throw new CustomException("自定义异常");}public class CustomException extends Exception {public CustomException(String message) {super(message);}}
}

解决方案:

  • 根据业务需求合理配置rollbackFor和noRollbackFor。通常情况下,@Transactional默认只会对RuntimeException和其子类进行回滚。如果你希望对其他类型的异常也进行回滚,可以使用rollbackFor指定具体的异常类型。
    改进后的代码:
  @Servicepublic class UserService {@Transactional(rollbackFor = {CustomException.class, RuntimeException.class})public void updateUser(User user) {// 执行更新操作userMapper.updateUser(user);// 抛出一个自定义异常throw new CustomException("自定义异常");}public class CustomException extends Exception {public CustomException(String message) {super(message);}}}

如果你不希望某些异常导致事务回滚,可以使用 noRollbackFor:

  @Servicepublic class UserService {@Transactional(noRollbackFor = CustomException.class)public void updateUser(User user) {// 执行更新操作userMapper.updateUser(user);// 抛出一个自定义异常throw new CustomException("自定义异常");}public class CustomException extends Exception {public CustomException(String message) {super(message);}}}

2、避免@Transactional注解失效策略

(1)、确保方法是public的。
(2)、避免在同一个类中直接调用其他事务方法(可以通过注入自身的方式来解决)。
(3)、正确配置事务管理器。
(4)、不要在事务方法内部捕获并处理可能导致事务回滚的异常(或者重新抛出一个Spring框架能够识别的异常)。
(5)、避免使用final和static修饰事务方法。
(6)、确保类被Spring管理。
(7)、正确配置事务的传播特性。
(8)、使用支持事务的数据库表和存储引擎。
(9)、在多线程环境下,确保事务方法在同一个线程中执行。

通过遵循这些原则,可以最大程度地确保@Transactional注解在Spring框架中的正确性和有效性。

3、@Transactional实现原理

在Spring框架中,@Transactional注解用于声明式事务管理,它通过AOP(面向切面编程)来实现。Spring使用代理机制来拦截带有@Transactional注解的方法调用,并在其周围添加事务管理逻辑。

(1)、Spring创建代理对象

当Spring容器启动时,它会扫描所有容器中带有@Transactional 注解的方法,并为这些方法创建一个代理对象。这个代理对象会拦截对原始业务逻辑类的调用,并在方法执行前后添加事务管理逻辑。

这个代理对象会在方法调用前后执行以下操作:

  • 开启事务:在方法执行之前,Spring会检查当前是否存在事务。如果不存在,则创建一个新的事务。
  • 提交或回滚事务:在方法执行完毕后,Spring会根据方法的执行结果决定是提交还是回滚事务。如果方法正常结束,则提交事务;如果方法抛出异常,则根据配置决定是否回滚事务。
  • 传播行为:@Transactional注解还支持多种传播行为(如REQUIRED、REQUIRES_NEW等),决定了如何处理现有事务或创建新事务。

创建代理对象的方式:

  • CGLIB动态代理(默认方式)
    如果目标类没有实现接口,Spring会使用CGLIB动态代理来生成代理类。CGLIB通过继承目标类并重写其方法来实现代理。
  • JDK动态代理
    如果目标类实现了接口,Spring会优先使用JDK动态代理。JDK动态代理通过实现接口并使用InvocationHandler来拦截方法调用。

(2)、@Transactional伪代码

下面是 @Transactional 代理机制的伪代码描述,展示了 Spring 如何通过代理对象来管理事务的生命周期。

示例:

// 假设这是 Spring 创建的代理对象
public class UserServiceProxy implements InvocationHandler {// 目标对象(原始的 UserService 实例)private final Object target;// 事务管理器private final PlatformTransactionManager transactionManager;// 事务属性(从 @Transactional 注解中读取)private final TransactionAttribute transactionAttribute;public UserServiceProxy(Object target, PlatformTransactionManager transactionManager, TransactionAttribute transactionAttribute) {this.target = target;this.transactionManager = transactionManager;this.transactionAttribute = transactionAttribute;}// 拦截对目标对象的方法调用@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 检查是否是需要事务管理的方法if (method.isAnnotationPresent(Transactional.class)) {return executeWithTransaction(method, args);} else {// 如果不是事务方法,直接调用目标对象的方法return method.invoke(target, args);}}// 执行带有事务管理的方法private Object executeWithTransaction(Method method, Object[] args) throws Throwable {// 1. 获取当前事务状态TransactionStatus status = null;try {// 2. 开启新事务或加入现有事务status = transactionManager.getTransaction(transactionAttribute);// 3. 调用目标对象的业务逻辑方法Object result = method.invoke(target, args);// 4. 提交事务transactionManager.commit(status);// 5. 返回业务方法的结果return result;} catch (Exception e) {// 6. 如果发生异常,回滚事务if (status != null) {transactionManager.rollback(status);}// 7. 抛出异常,让调用者处理throw e;} finally {// 8. 清理资源(如关闭连接等)// 这一步通常由连接池自动处理}}
}// 假设这是 Spring 容器中的代理工厂
public class TransactionalProxyFactory {public Object createProxy(Object target, Class<?>[] interfaces, PlatformTransactionManager transactionManager, TransactionAttribute transactionAttribute) {// 使用 JDK 动态代理if (interfaces.length > 0) {return Proxy.newProxyInstance(target.getClass().getClassLoader(),interfaces,new UserServiceProxy(target, transactionManager, transactionAttribute));}// 使用 CGLIB 动态代理else {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(target.getClass());enhancer.setCallback(new UserServiceProxy(target, transactionManager, transactionAttribute));return enhancer.create();}}
}

(3)、实例分析解释

通过使用自我注入(Self-Injection)方式来解释下:

示例:

@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate UserService self;  // 自我注入@Transactionalpublic void createUser(User user) {// 业务逻辑:插入用户数据userMapper.insertUser(user);// 通过代理对象调用 updateUser 方法self.updateUser(user);}@Transactionalpublic void updateUser(User user) {// 业务逻辑:更新用户数据userMapper.updateUser(user);}
}

解释:

  • Spring服务在创建容器时会自动扫描@Service,@Component,@Transactional等注解,并在容器中创建实例对象。在使用的地方通过@Autowired或@Resource注解可以拿出该对象的代理对象使用(注意此时拿出来的是代理对象,而不是原始对象)。
  • 针对@Transactional注解,spring内部通过aop的方式对注解方法做了围绕增强,如帮我们开启事务,结束帮我们提交事务等,如第二部分的伪代码。
  • 在本例中,本身就是UserService,内部在注入UserService self对象,这两个对象都是实现一样的功能,只不过self对象通过@Autowired注入,实际为UserService的代理对象。
    如果类中直接使用自身的updateUser方法,属于内部直接调用的范畴。如果使用self对象调用updateUser方法,则是通过代理对象实现的。代理对象会执行aop使事务生效。内部调用没有调用aop,所以事务就不会起作用了。

乘风破浪会有时,直挂云帆济沧海!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/893128.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识

20250118拿掉荣品pro-rk3566开发板上Android13下在uboot和kernel启动阶段的Rockchip这个LOGO标识 2025/1/18 15:12 缘起&#xff1a;做飞凌OK3588-C开发板/核心板【Linux R4】的时候&#xff0c;测试/生产要求没有开机LOGO【飞凌/Rockchip】 要求&#xff1a;黑屏或者中性界面。…

【转】厚植根基,同启新程!一文回顾 2024 OpenHarmony 社区年度工作会议精彩瞬间

在数字化浪潮奔腾不息的今天&#xff0c;开源技术已成为推动科技创新与产业发展的强大引擎。2025年1月10日-11日&#xff0c;OpenAtom OpenHarmony&#xff08;开放原子开源鸿蒙&#xff0c;以下简称“OpenHarmony”或“开源鸿蒙”&#xff09;社区2024年度工作会议于深圳盛大启…

Mybatis 进阶 / Mybatis—Puls (详细)

目录 一.动态SQL 1.1标签 1.2 标签 1.3标签 1.4标签 1.5标签 1.6标签 mybatis总结&#xff1a; 二.Mybatis-Puls 2.1准备工作 2.2CRUD单元测试 2.2.1创建UserInfo实体类 2.2.2编写Mapper接⼝类 2.2.3 测试类 2.3 常见注解 2.3.1TableName 2.3.2TableField 2.4打印日…

Go 切片:用法和本质

要想更好的了解一个知识点&#xff0c;实战是最好的经历。 题目 我这里放一道题目&#xff1a; package mainimport "fmt"func SliceRise(s []int) {s append(s, 0)for i : range s {s[i]}fmt.Println(s) }func SlicePrint() {s1 : []int{1, 2}s2 : s1s2 append…

如何下载对应城市的地理json文件

这里采用的是阿里地图工具进行查找&#xff1a; DataV.GeoAtlas地理小工具系列 由阿里云DataV数据可视化团队出品,多年深耕数据可视化领域,数据大屏业务开拓者和领航者。致力用震撼而清晰的视觉语言,让更多人读懂大数据,受惠数据驱动的决策方式 第一步打开网站 &#xff1a; …

AI 大爆发时代,音视频未来路在何方?

AI 大模型突然大火了 回顾2024年&#xff0c;计算机领域最大的变革应该就是大模型进一步火爆了。回顾下大模型的发展历程&#xff1a; 萌芽期&#xff1a;&#xff08;1950-2005&#xff09; 1956年&#xff1a;计算机专家约翰麦卡锡首次提出“人工智能”概念&#xff0c;标志…

解决wordpress媒体文件无法被搜索的问题

最近,我在wordpress上遇到了一个令人困扰的问题:我再也无法在 WordPress 的媒体库中搜索媒体文件了。之前,搜索媒体非常方便,但现在无论是图片还是其他文件,似乎都无法通过名称搜索到。对于我这样需要频繁使用图片的博主来说,这简直是个大麻烦。 问题源头 一开始,我怀…

代码随想录训练营第五十一天| 99.岛屿数量 深搜 岛屿数量 广搜 100.岛屿的最大面积

99.岛屿数量 深搜 题目链接&#xff1a;99. 岛屿数量 讲解链接&#xff1a;代码随想录 就是dfs模版题目 在dfs里可以先定义方向数组移动 再遍历分别向四个方向移动 同时记得更新当前nextx nexty 再判断是否越界 再执行判断条件 当前位置未走过 visited[i][j] false 一开始jav…

springboot之YAML语法

目录 一、基本语法 写一个端口号和一个路径 Controller里的方法&#xff1a; 然后这样写才能访问到&#xff1a; 这是在yml里面写的&#xff0c;也可以写在properties里 再访问&#xff1a; 二、值的写法 1.普通类型(数字、字符串、布尔) 例子1&#xff1a; 配置文件…

ASP .NET Core 学习 (.NET 9)- 创建 API项目,并配置Swagger及API 分组或版本

本系列为个人学习 ASP .NET Core学习全过程记录&#xff0c;基于.NET 9 和 VS2022 &#xff0c;实现前后端分离项目基础框架搭建和部署&#xff0c;以简单、易理解为主&#xff0c;注重页面美观度和后台代码简洁明了&#xff0c;可能不会使用过多的高级语法和扩展&#xff0c;后…

LuaJIT Garbage Collector Algorithms

Explain 本篇文章是对Make Pall发表wili内容《LuaJIT 3.0 new Garbage Collector》的翻译和扩展&#xff0c;因为原文是对LuaJIT 2.x GC重要功能的简介和对LuaJIT 3.0 new GC的工作计划&#xff0c;所以它并不是系统性介绍GC的文章。希望以后能有精力系统性的对LuaJIT 2.x GC做…

ChatGPT大模型极简应用开发-CH1-初识 GPT-4 和 ChatGPT

文章目录 1.1 LLM 概述1.1.1 语言模型和NLP基础1.1.2 Transformer及在LLM中的作用1.1.3 解密 GPT 模型的标记化和预测步骤 1.2 GPT 模型简史&#xff1a;从 GPT-1 到 GPT-41.2.1 GPT11.2.2 GPT21.2.3 GPT-31.2.4 从 GPT-3 到 InstructGPT1.2.5 GPT-3.5、Codex 和 ChatGPT1.2.6 …

基于单片机的直流电机控制系统(论文+源码)

1 系统方案设计 本设计基于单片机的直流电机控制系统的总体架构设计如图2.1所示&#xff0c;其采用STM32F103单片机作为控制器&#xff0c;结合ESP8266 WiFi通信模块、L9110电机驱动电路、OLED液晶、按键等构成整个系统。用户在使用时&#xff0c;可以通过按键或者手机APP设定直…

【Linux】Linux入门(2)常见指令

目录 Linux下的文件ls 指令 --- 展示目录pwd指令 --- 显示当前目录cd 指令 --- 改变工作目录touch指令 --- 创建普通文件stat指令 --- 查看文件属性mkdir指令 --- 创建目录rmdir指令 --- 删除目录rm指令 --- 同时删除文件或目录man指令 --- 访问帮助手册cp指令 复制文件或目录m…

《自动驾驶与机器人中的SLAM技术》ch4:基于预积分和图优化的 GINS

前言&#xff1a;预积分图优化的结构 1 预积分的图优化顶点 这里使用 《自动驾驶与机器人中的SLAM技术》ch4&#xff1a;预积分学 中提到的散装的形式来实现预积分的顶点部分&#xff0c;所以每个状态被分为位姿&#xff08;&#xff09;、速度、陀螺零偏、加计零偏四种顶点&am…

BMC知识框图

OpenBMC简介 OpenBMC作为BMC的Linux发行版&#xff0c;旨在管理广泛系统&#xff0c;如企业、高性能计算、电信和大规模数据中心。 BMC&#xff0c;或Baseboard Manager Controller&#xff0c;是服务器主板上的专用微控制器&#xff0c;采用IPMI架构&#xff0c;具备智能性&…

书生大模型基础岛第五关

基础任务&#xff1a;使用 XTuner 微调 InternLM2-Chat-7B 实现自己的小助手认知&#xff0c;如下图所示&#xff08;图中的尖米需替换成自己的昵称&#xff09;&#xff0c;记录复现过程并截图。 1.配置环境 2.修改数据&#xff0c;将尖米修改为人工智能小助手 修改之前 修改…

EI Scopus双检索 | 2025年第四届信息与通信工程国际会议(JCICE 2025)

会议简介 Brief Introduction 2025年第四届信息与通信工程国际会议(JCICE 2025) 会议时间&#xff1a;2025年7月25日-27日 召开地点&#xff1a;中国哈尔滨 大会官网&#xff1a;www.jcice.org 由黑龙江大学和成都信息工程大学主办&#xff0c;江苏科技大学协办的2025年第四届信…

于灵动的变量变幻间:函数与计算逻辑的浪漫交织(下)

大家好啊&#xff0c;我是小象٩(๑ω๑)۶ 我的博客&#xff1a;Xiao Xiangζั͡ޓއއ 很高兴见到大家&#xff0c;希望能够和大家一起交流学习&#xff0c;共同进步。 这一节我们主要来学习单个函数的声明与定义&#xff0c;static和extern… 这里写目录标题 一、单个函数…

Python Pyside6 加Sqlite3 写一个 通用 进销存 系统 初型

图: 说明: 进销存管理系统说明文档 功能模块 1. 首页 显示关键业务数据商品总数供应商总数本月采购金额本月销售金额显示预警信息库存不足预警待付款采购单待收款销售单2. 商品管理 商品信息维护商品编码(唯一标识)商品名称规格型号单位分类进货价销售价库存数量预警…