总监:喂,小王啊!起来没呢?加个班呗!

我:泥煤啊…

总监:我有个需求啊,这最近导入数据比较多,但是后台用户反映导入了数据,不想要了,删除起来麻烦啊!你也知道,顾客是上帝嘛,给我完成一个导入数据自动一键回滚的功能!

我:说啥也不干,今天休息,我还要打游戏。

总监:那个你申请一个在家办公,两倍工资,我这面批一个。

我:好嘞!


一:需求分析

  1. 导入一定分为很多种,有商品的,有图片的,有各种业务的,一定要兼容各种具体的业务,那么就不能依赖于具体实现。
  2. 分析在各个业务层,导入无谓就是处理完数据之后生成的增删改语句。
  3. 那我只需要处理sql语句就可以了,把增删改的语句生成它具体的相反的语句。insert生成delete语句,udapte生成delete和insert语句,delete生成insert语句。
  4. 那么多的mapper层接口的语句,怎么知道哪个语句是需要生成相反的语句呢?可以自定义一个注解,然后我们在执行之前看看该接口上面有没有这个注解就行了。
  5. 那在并行多次导入的时候怎么区分哪些任务是属于同一任务的呢?这么办,运用线程标识该次任务。那开启多线程怎么办呢?开启多线程就把每一个线程都存入任务名称。
  6. 好啦,差不多思路就是这些,总结一下就是在sql执行之前,拦截要执行的sql。判定该要执行的sql的mapper层接口是否有自定义约定的注解,如果有,那么该语句是需要生成相反sql的。判定该线程中是否存储有任务名称,有则生成相反语句并存储到redis中,该任务名称为redis中的key。value采用list结构,我们从左向右添加,要是执行的话,也是从左边进行执行。

二:代码编写

  1. 首先自定义一个注解,EnableReverseSql

    @Target({ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EnableReverseSql {}
    
  2. 定义一个生成反向sql的顶级接口,以后用于适配不同的数据库

    public interface ReverseSqlDb {String getSql(Statement statement);String  insertGenerateDelete(Invocation invocation,String sql);List<String> updateGenerateDeleteAndInsert(Invocation invocation,String sql,String className);String deleteGenerateInsert(Invocation invocation,String sql,String className);String getDbVersion();}
    
  3. 因为要生成反向sql,比如删除,只会知道id,那么我们必须要查询该id的所有信息,才能生成insert语句,这里定义一个mapper接口,用于执行在代码中生成的查询语句。在有@ReverseSqlDb注解的接口层必须继承该类。

    public interface ReverseMapper {@Select("${sql}")@InterceptorIgnore(tenantLine = "true")LinkedHashMap<String,Object> performSql(@Param("sql") String sql);
    }
    

    例:

    @Mapper
    public interface TestMapper extends ReverseMapper {}
    

    这样我们就可以执行代码中任意生成的sql语句。

  4. 核心思想,要拦截sql,就需要实现Interceptor接口,用于拦截需要执行的语句,新建ReverseSqlInterceptor

    @Slf4j
    @Component
    @Intercepts({@Signature(type = StatementHandler.class, method = "update", args = Statement.class),@Signature(type = StatementHandler.class, method = "batch", args = Statement.class)
    })
    public class ReverseSqlInterceptor implements Interceptor {@ResourceReverseSqlDbChainOfResponsibility reverseSqlDbChainOfResponsibility;@AutowiredSpringConfigProperties springProperties;/*** 获取当前在使用的数据库* @return 数据库名称*/private String getDbVersion(){String druidUrl;//这里只是为了适配不同数据源进行的判断,最终只是要过去正在使用的是什么数据库if( springProperties.getDataSource().getDruid() == null){druidUrl = springProperties.getDataSource().getUrl();}else{druidUrl = springProperties.getDataSource().getDruid().getUrl();}return druidUrl.split(":")[1];}@Overridepublic Object intercept(Invocation invocation) throws Throwable {//获取Statement类对象Statement statement = this.getStatement(invocation);Object target = PluginUtils.realTarget(invocation.getTarget());MetaObject metaObject = SystemMetaObject.forObject(target);MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");//获取命名空间String namespace = mappedStatement.getId();//获取类名String className = namespace.substring(0, namespace.lastIndexOf("."));//获取当前类的方法名String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);//获取当前类有哪些方法Method[] ms = Class.forName(className).getMethods();for (Method m : ms) {if (m.getName().equals(methodName)) {//判断是否有这个注解Annotation annotation = m.getAnnotation(EnableReverseSql.class);if (annotation != null) {//通过反射redis类实例对象并获取Method getMethod = InternalThreadLocal.getMethodForNameGet();HashMap<String, Object> stringObjectHashMap  = (HashMap<String, Object>) getMethod.invoke(InternalThreadLocal.treadUtilEntity);String taskName = (String) stringObjectHashMap.get("name");if(taskName==null){throw new RuntimeException("未获取到线程中的任务名称,请添加任务名称");}reverseSqlDbChainOfResponsibility.selectDbAndExecuteChain(getDbVersion(),invocation,statement,className,taskName);} else {return invocation.proceed();}}}return invocation.proceed();}/*** ThreadLocal内部静态类*/private static class InternalThreadLocal{private static Class<?> treadUtilClass;private static Constructor<?> declaredConstructor;private static Object treadUtilEntity;private static Class[] treadUtilArguments;/*** 加载ThreadUtil工具类*/static {try {treadUtilClass = Class.forName("com.common.util.ThreadUtil");declaredConstructor = treadUtilClass.getDeclaredConstructor();//强制使用私有的构造方法declaredConstructor.setAccessible(true);treadUtilEntity = declaredConstructor.newInstance();treadUtilArguments = new Class[0];} catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {log.error("获取ThreadUtil工具类失败");e.printStackTrace();}}private static Method getMethodForNameGet() throws NoSuchMethodException {return treadUtilClass.getMethod("get",treadUtilArguments);}}/*** 获取statement*/private Statement getStatement(Invocation invocation) {Statement statement;Object firstArg = invocation.getArgs()[0];if (Proxy.isProxyClass(firstArg.getClass())) {statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");} else {statement = (Statement) firstArg;}MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);try {statement = (Statement) stmtMetaObj.getValue("stmt.statement");} catch (Exception e) {//这个位置不需要捕获异常,会报错}if (stmtMetaObj.hasGetter("delegate")) {try {statement = (Statement) stmtMetaObj.getValue("delegate");} catch (Exception e) {//这个位置不需要捕获异常,会报错}}if(statement != null){return statement;}else{throw new RuntimeException("未获取到Statement类");}}}
    
  5. 为了以后适配更多的数据库,新建ReverseSqlDbChainOfResponsibility类,用于适配不同的数据库

    @Component
    public class ReverseSqlDbChainOfResponsibility implements CommandLineRunner, ApplicationContextAware {private Collection<ReverseSqlDb> reverseSqlDbList;private volatile ApplicationContext applicationContext;@Overridepublic void run(String... args) throws Exception {init();}private void init() {reverseSqlDbList = new LinkedList<>(this.applicationContext.getBeansOfType(ReverseSqlDb.class).values());}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext=applicationContext;}@SneakyThrowsvoid selectDbAndExecuteChain(String dbVersion,Invocation invocation,Statement statement,String className,String taskName){//反射获取列表的又添加Method lSetMethod = RedisMethod.getLrSetObj();//获取列表的右添加列表Method lSetListMethod = RedisMethod.getLrSetList();for (ReverseSqlDb reverseSqlDb:reverseSqlDbList) {if(reverseSqlDb instanceof Proxy){continue;}if(dbVersion.equals(reverseSqlDb.getDbVersion())){//判断sql是要执行增删改中的哪一个方法String sql = reverseSqlDb.getSql(statement);if (sql.contains("INSERT") || sql.contains("insert")) {//调用新增方法生成反向sqlString reverseSql = reverseSqlDb.insertGenerateDelete(invocation, sql);lSetMethod.invoke(RedisMethod.redisEntity,taskName,reverseSql);} else if (sql.contains("UPDATE") || sql.contains("update")) {//调用修改方法生成反向sqlList<String> reverseSqlList = reverseSqlDb.updateGenerateDeleteAndInsert(invocation, sql, className);lSetListMethod.invoke(RedisMethod.redisEntity,taskName,reverseSqlList);} else if (sql.contains("DELETE") || sql.contains("delete")) {//调用删除方法生成反向sqlString reverseSql = reverseSqlDb.deleteGenerateInsert(invocation, sql, className);lSetMethod.invoke(RedisMethod.redisEntity,taskName,reverseSql);}}}}
    }
    
  6. 编写具体的实现类ReverseSqlDbPg

    @Slf4j
    @Component
    public class ReverseSqlDbPg extends AbstractBusiness implements ReverseSqlDb {private static final String DRUID_POOLED_PREPARED_STATEMENT = "com.alibaba.druid.pool.DruidPooledPreparedStatement";private static final String T4C_PREPARED_STATEMENT = "oracle.jdbc.driver.T4CPreparedStatement";private static final String ORACLE_PREPARED_STATEMENT_WRAPPER = "oracle.jdbc.driver.OraclePreparedStatementWrapper";private Method oracleGetOriginalSqlMethod;private Method druidGetSqlMethod;static final String DB_VERSION = "postgresql";/*** 获取当前正在执行的sql** @param statement 声明* @return 当前要执行的语句*/@Overridepublic String getSql(Statement statement) {String originalSql = null;String stmtClassName = statement.getClass().getName();if (DRUID_POOLED_PREPARED_STATEMENT.equals(stmtClassName)) {try {if (druidGetSqlMethod == null) {Class<?> clazz = Class.forName(DRUID_POOLED_PREPARED_STATEMENT);druidGetSqlMethod = clazz.getMethod("getSql");}Object stmtSql = druidGetSqlMethod.invoke(statement);if (stmtSql instanceof String) {originalSql = (String) stmtSql;}} catch (Exception e) {e.printStackTrace();}} else if (T4C_PREPARED_STATEMENT.equals(stmtClassName)|| ORACLE_PREPARED_STATEMENT_WRAPPER.equals(stmtClassName)) {try {if (oracleGetOriginalSqlMethod != null) {Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement);if (stmtSql instanceof String) {originalSql = (String) stmtSql;}} else {Class<?> clazz = Class.forName(stmtClassName);oracleGetOriginalSqlMethod = getMethodRegular(clazz, "getOriginalSql");if (oracleGetOriginalSqlMethod != null) {oracleGetOriginalSqlMethod.setAccessible(true);if (null != oracleGetOriginalSqlMethod) {Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement);if (stmtSql instanceof String) {originalSql = (String) stmtSql;}}}}} catch (Exception e) {//ignore}}if (originalSql == null) {originalSql = statement.toString();}originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);int index = indexOfSqlStart(originalSql);if (index > 0) {originalSql = originalSql.substring(index);}return originalSql;}/*** 新增生成删除** @param invocation* @param sql        要执行的sql* @return 生成的反向sql*/@Overridepublic String insertGenerateDelete(Invocation invocation, String sql) {List<String> paramTerList = this.getParamTerList(invocation);//添加的参数列表第一位是id,我们就默认第一位是id,添加的话只需要反向生成删除的sql即可//获取要删除的表名,添加语句的insert into 表名,所以这里取列表中的第三位String[] words = sql.split(" ");String tableName = words[2];//拼接反向sqlStringBuilder stringBuilder = new StringBuilder();stringBuilder.append("delete from ");stringBuilder.append(tableName);stringBuilder.append(" where id = ");stringBuilder.append(paramTerList.get(0));return stringBuilder.toString();}/*** 修改方法生成删除和新增方法实现*/@Overridepublic List<String> updateGenerateDeleteAndInsert(Invocation invocation, String sql, String className) {ArrayList<String> resList = new ArrayList<>();List<String> paramTerList = this.getParamTerList(invocation);//修改的语句最后的参数为id,默认最后的条件为id.表明则为单词的第二个单词,由此获得id与表名String[] words = sql.split(" ");String tableName = words[1];String id = paramTerList.get(paramTerList.size() - 1);//生成删除语句StringBuffer deleteBuffer = new StringBuffer();deleteBuffer.append("delete from ");deleteBuffer.append(tableName);deleteBuffer.append(" where id = ");deleteBuffer.append(id);//修改我们需要查询该id的所有数据,这里通过反射注入该接口,并通过继承的方式必须实现我们规定的接口,从而执行拼接的查询sqlLinkedHashMap<String, Object> resMap = this.getMapById(className, tableName, id);//获取所有的valueSet<String> keys = resMap.keySet();//拼接新增语句StringBuffer insertBuffer = new StringBuffer();insertBuffer.append("insert into ");insertBuffer.append(tableName);insertBuffer.append(" values (");for (String key : keys) {if (resMap.get(key) != null) {insertBuffer.append("'");insertBuffer.append(resMap.get(key));insertBuffer.append("'");insertBuffer.append(",");} else {insertBuffer.append(resMap.get(key));insertBuffer.append(",");}}insertBuffer.deleteCharAt(insertBuffer.length() - 1);insertBuffer.append(")");//结果添加到列表resList.add(deleteBuffer.toString());resList.add(insertBuffer.toString());log.info(resMap.toString());return resList;}/***  删除语句生成新增的具体执行方法*/@Overridepublic String deleteGenerateInsert(Invocation invocation, String sql, String className) {List<String> paramTerList = this.getParamTerList(invocation);//修改的语句最后的参数为id,默认最后的条件为id.表名则为单词的第三个单词,由此获得id与表名String[] words = sql.split(" ");String tableName = words[2];String id = paramTerList.get(paramTerList.size()-1);LinkedHashMap<String, Object> resMap = this.getMapById(className, tableName, id);//获取所有的valueSet<String> keys = resMap.keySet();//拼接新增语句StringBuffer insertBuffer = new StringBuffer();insertBuffer.append("insert into ");insertBuffer.append(tableName);insertBuffer.append(" values (");for (String key:keys) {if(resMap.get(key)!=null){insertBuffer.append("'");insertBuffer.append(resMap.get(key));insertBuffer.append("'");insertBuffer.append(",");}else{insertBuffer.append(resMap.get(key));insertBuffer.append(",");}}insertBuffer.deleteCharAt(insertBuffer.length()-1);insertBuffer.append(")");insertBuffer.append(" on CONFLICT(id) do NOTHING ");return insertBuffer.toString();}/*** 获取当前执行器是哪个执行器* @return 执行器名称*/@Overridepublic String getDbVersion() {return DB_VERSION;}/*** 通过反射获取接口并执行继承的方法*/private LinkedHashMap<String, Object> getMapById(String className, String tableName, String id) {Class<? extends ReverseMapper> serviceClass;try {serviceClass = (Class<? extends ReverseMapper>) Class.forName(className);} catch (Exception e) {throw new RuntimeException("如使用**注解,请继承ReverseMapper接口");}//生成查询语句StringBuffer selectBuffer = new StringBuffer();selectBuffer.append("select * from ");selectBuffer.append(tableName);selectBuffer.append(" where id = ");selectBuffer.append(id);//反射调用规定好的方法return super.getMapper(serviceClass).performSql(selectBuffer.toString());}/*** 获取该语句的参数列表*/private List<String> getParamTerList(Invocation invocation) {Object target = PluginUtils.realTarget(invocation.getTarget());MetaObject metaObject = SystemMetaObject.forObject(target);// 参数BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");Object parameterObject = boundSql.getParameterObject();List<ParameterMapping> parameterMappings = new ArrayList<>(boundSql.getParameterMappings());if (parameterMappings.isEmpty() && parameterObject == null) {log.warn("parameterMappings is empty or parameterObject is null");}MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");Configuration configuration = mappedStatement.getConfiguration();TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();List<String> parameterList = new ArrayList<>();MetaObject newMetaObject = configuration.newMetaObject(parameterObject);for (ParameterMapping parameterMapping : parameterMappings) {String parameter = null;if (parameterMapping.getMode() == ParameterMode.OUT) {continue;}String propertyName = parameterMapping.getProperty();if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {parameter = getParameterValue(parameterObject);} else if (newMetaObject.hasGetter(propertyName)) {parameter = getParameterValue(newMetaObject.getValue(propertyName));} else if (boundSql.hasAdditionalParameter(propertyName)) {parameter = getParameterValue(boundSql.getAdditionalParameter(propertyName));}parameterList.add(parameter);}return parameterList;}/*** 获取参数** @param param Object类型参数* @return 转换之后的参数*/private static String getParameterValue(Object param) {if (param == null) {return "null";}if (param instanceof Number) {return param.toString();}String value = param.toString();return StringUtils.quotaMark(value);}/*** 获取此方法名的具体 Method** @param clazz      class 对象* @param methodName 方法名* @return 方法*/private Method getMethodRegular(Class<?> clazz, String methodName) {if (Object.class.equals(clazz)) {return null;}for (Method method : clazz.getDeclaredMethods()) {if (method.getName().equals(methodName)) {return method;}}return getMethodRegular(clazz.getSuperclass(), methodName);}/*** 获取sql语句开头部分** @param sql ignore* @return ignore*/private int indexOfSqlStart(String sql) {String upperCaseSql = sql.toUpperCase();Set<Integer> set = new HashSet<>();set.add(upperCaseSql.indexOf("SELECT "));set.add(upperCaseSql.indexOf("UPDATE "));set.add(upperCaseSql.indexOf("INSERT "));set.add(upperCaseSql.indexOf("DELETE "));set.remove(-1);if (CollectionUtils.isEmpty(set)) {return -1;}List<Integer> list = new ArrayList<>(set);list.sort(Comparator.naturalOrder());return list.get(0);}}
    
    1. 这里在获取mapper接口中个各个方法的时候,为了防止具体使用者没有继承ReverseMapper接口,写了一个AbstractBusiness类抽象类。代码如下

      public abstract class AbstractBusiness {@ResourceBeanHelper helper;public void setMapper(BeanHelper helper) {this.helper = helper;}public BeanHelper getMapper() {return helper;}public final <T extends ReverseMapper> T getMapper(Class<T> t){return helper.getBean(t);}}
      
    2. BeanHelper代码

      @Component
      public class BeanHelper implements ApplicationContextAware {public static ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {BeanHelper.applicationContext = applicationContext;}public <T extends ReverseMapper> T getBean(Class<T> t) {return applicationContext.getBean(t);}public Object getSpringBean(String s) {return  applicationContext.getBean(s);}
      }
      
  7. 其他帮助类

    @Slf4j
    public class RedisMethod {private static Class<?> redisClass;public static Object redisEntity;static {try {redisClass = Class.forName("com.component.redis.Redis");if(redisClass == null){throw new RuntimeException("获取不到redis工具类,请引入包后重试");}redisEntity = redisClass.newInstance();} catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {log.error("获取Redis工具类失败");e.printStackTrace();}}/*** 获取redis工具类右添加obj的方法* @return 方法*/@SneakyThrowspublic static Method getLrSetObj(){Class[] rightPushArguments = new Class[2];rightPushArguments[0] = String.class;rightPushArguments[1] = Object.class;return redisClass.getMethod("lrSetObj",rightPushArguments);}/*** 获取又添加列表的方法* @return 方法*/@SneakyThrowspublic static Method getLrSetList(){Class[] rightPushListArguments = new Class[2];rightPushListArguments[0] = String.class;rightPushListArguments[1] = List.class;return redisClass.getMethod("lrSetList",rightPushListArguments);}
    }
    

工具类在这里,redis工具类

@Slf4j
public class ThreadUtil {private ThreadUtil(){ }private static final ThreadLocal threadLocal = new ThreadLocal<>();public static void set(Object o) {threadLocal.set(o);}public static Object get() {return threadLocal.get();}public static void remove(){threadLocal.remove();}}

三:代码执行

  1. public class TestController {@Resourceprivate TestMapper testMapper;public void test(){//此处放入线程的为任务名称,根据业务自行调整,多线程时也需要放入线程中该任务名称ThreadUtil.set("test");testMapper.insert();ThreadUtil.remove();}
    }
    
  2. 最后线程执行结束的时候,不要忘了调用ThreadUtil.remove()方法删除,删除线程中数据。

  3. 思路就是这样,代码还有优化的空间。可自行修改。

四:注意事项

  1. 当前代码只支持一条条插入,一条条的删除,一条一条的修改,而且只能是最基本的增删改,要使用此功能的话需要编写对应接口的标准的增删改语句,标准语句的格式在代码中有所描述。
  2. 数据在更新之前都会进行一次查询,速度会响应的减慢。
  3. 生成的反向sql是存储在redis中的,可以把redis中的key存储在数据库,(反向的sql已经有了,怎么用,怎么执行看自己的业务),redis中的数据可以在下一次导入之前进行删除。

我:喂,总监啊,搞定了啊!

总监:不错,小伙子。

导入数据java生成逆向sql,用于回滚,你试过吗?相关推荐

  1. 如何从Excel表格导入数据批量生成二维码

    目前二维码应用渐趋广泛,二维码具有储存量大.保密性高.追踪性高.抗损性强.备援性大.成本便宜等特性,这些特性特别适用于表单.安全保密.追踪.证照.存货盘点.资料备援等方面.那么我们怎么用条码打印软件从 ...

  2. 如何从Excel表格导入数据批量生成二维码 1

    目前二维码应用渐趋广泛,二维码具有储存量大.保密性高.追踪性高.抗损性强.备援性大.成本便宜等特性,这些特性特别适用于表单.安全保密.追踪.证照.存货盘点.资料备援等方面.那么我们怎么用条码打印软件从 ...

  3. mysql 事务回滚语句_数据库事务回滚语句-sql事务回滚语句是-用于事务回滚的sql语句...

    sql 回滚语句 这种情况的数据恢复只能利用事务日志的备份来进行,所以如果你的SQL没有进行相应的全库备份 或不能备份日志(truncate log on checkpoint选项为1),那幺就无法进 ...

  4. python导入csv文件-Python从CSV文件导入数据和生成简单图表

    原标题:Python从CSV文件导入数据和生成简单图表 我们已经完成Python的基础环境搭建工作,现在我们尝试导入CSV数据 我们准备一个csv测试数据,文件名是csv-test-data.csv数 ...

  5. Excel导入数据轻松生成智能图表,助力数据分析

    运营助手,Excel导入数据轻松生成智能图表,助力数据分析 2023-04-18 10:21·淡定海风L 智能问答BI是一种先进的数据分析,它可以帮助用户快速地从海量数据中获取有用的信息,并将其可视化 ...

  6. mysql 导入数据库sql语句_mysql中导入数据与导出数据库sql语句

    本文章来详细介绍关于mysql中导入数据与导出数据库sql语句,在mysql中常用的导入与导出数据的命令有source与mysqldump大家可参考. 1.例1:连接到本机上的MYSQL 首先在打开D ...

  7. mysql重做日志恢复数据_MySQL中重做日志,回滚日志,以及二进制日志的简单总结...

    MySQL中有六种日志文件, 分别是:重做日志(redo log).回滚日志(undo log).二进制日志(binlog).错误日志(errorlog).慢查询日志(slow query log). ...

  8. mysql latid1_【转】mysql触发器的实战经验(触发器执行失败,sql会回滚吗) | 学步园...

    1   引言Mysql的触发器和存储过程一样,都是嵌入到mysql的一段程序.触发器是mysql5新增的功能,目前线上凤巢系统.北斗系统以及哥伦布系统使用的数据库均是mysql5.0.45版本,很多程 ...

  9. [转]SQL事务回滚的问题及其解决的方法

    [转]SQL事务回滚的问题及其解决的方法 原文:http://shirlly.javaeye.com/blog/370973 Begin Transaction:开始一个事务: Commit Tran ...

  10. java更新数据库错误就回滚_Java 中对数据库操作时的 回滚

    Connection conn=null; conn.rollback()就可以回滚 //用jdbc连接数据库 //举例子,比如你在写一个级联删除的方法的时候,为了保证数据完整性,删除的时候一定要确定 ...

最新文章

  1. Linux内核设计与实现学习笔记目录
  2. 如何5分钟秒懂Java之基础入门篇 第一个hello word
  3. python列表求平均值_python与统计概率思维
  4. Android开发之EditText输入显示文字hint大小设置
  5. 【Java每日一题】20161018
  6. Linux环境 Jenkins集成构建SonarQube
  7. 监控硬盘脚本linux,shell脚本实现磁盘监控系统
  8. 我的MVVM框架 v3教程——todos例子
  9. 正交表生成工具allpairs的使用
  10. 异步社区本周新上电子书
  11. 西南石油大学本科毕业论文答辩PPT模板
  12. 【无标题】win7系统怎么配置adb环境变量
  13. 简单代码变出超个性化的QQ昵称
  14. 北京大学开设电子游戏选修课,火“爆”到没地方坐
  15. CorelDRAW破解版是如何一步一步坑人的
  16. [Pandas技巧] 多列值合并成一列
  17. win10网上邻居无法显示计算机,w10网上邻居看不到其他电脑的解决方法[多图]
  18. 计算机外文文献论文翻译,外文文献+翻译--计算机专业论文
  19. qt平台集成google拼音中文输入法
  20. HTTP协议和XMPP协议

热门文章

  1. gbk与gb2312的区别是什么?
  2. 一键生成所有尺寸App Icon
  3. 如何查询Linux软件安装源,Zypper——suse软件查询 安装 升级 与 软件源编辑
  4. Lua开发工具(IntelliJ IDE +EmmyLua 插件 )
  5. 编译原理实验-PL0自底向上语法分析
  6. (亲测)使用cmd结束进程的3种方法
  7. 密码分析(一):差分密码分析
  8. 宝峰c1对讲机写频软件_宝峰对讲机写频软件下载7.01 官方正式版-宝峰BF480,BF520,F25,F26对讲机写频软件西西软件下载...
  9. 微信逆向:如何统计好友添加数据和聊天记录数据?
  10. 第三章 DirectX 图形绘制(上)