JDBC综合训练——数据同步

项目任务背景

某高校预计教务管理系统一套,本公司[OA教务系统]功能覆盖高校提出的大部分需求,仍然有部分客户重要需求无法满足。无法满足的需求如下: 1、[教务系统]需要实现学生评奖评优的审核工作,但是学生申请是通过[XG学工管理系统]进行提交,学生通过[学工系统]可以查询评审进度; 2、[教务系统]完成评奖评优审核工作之后,需要进行公示,学生通过[学工系统]可以查询评奖评优公示; 3、学生可以针对公示进行质疑,学工处质可以查询疑结果。

公司已经将该客户列为重点客户,需要业务及技术团队攻克难题。针对无法满足的需求需要技术团队进行技术预演,为公司拿下该[教务系统]项目提供技术支撑。

初步了解: 1、[学工系统]使用MYSQL5.0数据库,与本公司[教务系统]使用的数据库及版本一致 2、[学工系统]学生评奖评优申请设计的表结构有: data_xsxx(学生信息):主键、学号、姓名、班级、专业、院校、性别、出生日期、入学时间、民族 data_pjsq(评奖申请):主键、申请奖项、学生主键、申请时间、审核状态 data_xscj(学生成绩):主键、学生主键、学科、学科成绩、录入时间 data_pjgs(评奖公示):主键、申请主键、公示备注、公示时间 data_pjzy(评奖质疑):主键、公示主键、质疑说明、质疑时间 3、申请“审核状态”说明: 0:未提交申请(草稿状态) 100:待班主任审核 200:待分院学工评优管理员审核 300:待校学工评优管理员审核 400:审核通过 -100:班主任退回 -200:分院学工评优管理员退回 -300:校学工评优管理员退回 3、学生评奖评优的审核工作流程环节:班主任==》分院学工评优管理员 ==》校学工评优管理员

技术预演内容: 实现教务管理系统与[学工系统]关于学生评奖评优功能的数据传输及同步

预演预期效果: 1、[学工系统]data_xsxx(学生信息)、data_xscj(学生成绩)、data_pjzy(评奖质疑),在进行新增或者变更时,需要在教务管理系统数据库中相同数据库表进行相应变化;

2、[学工系统]data_pjsq(评奖申请)在提交审核时,需要在教务管理系统中新增相应变化;

3、教务管理OA系统data_pjsq(评奖申请)进行审核时,需要在[学工系统]中变更相应变化;

4、教务管理OA系统data_pjgs(评奖公示),在进行新增或者变更时,需要在[学工系统]系统中进行相应变化;

需求分析

有两个数据库,xg系统和oa系统。要实现这两个数据库之间的数据同步。学⼯系统在进⾏新增或者变更时,需要在OA系统中进⾏相应变化; 需要的技术储备:(jdbc , 数据库,多线程,连接池,集合)

数据库部分脚本

#新建数据库
Create database oa;
Create database xg;
​
#数据库查询语句
drop table if EXISTS data_pjzy;
drop table if EXISTS data_pjgs;
DROP TABLE if EXISTS data_pjsq;
DROP TABLE if EXISTS data_xscj;
DROP TABLE if EXISTS data_xsxx;
​
#学生信息表
CREATE table data_xsxx(
id VARCHAR(32) PRIMARY KEY COMMENT '主键',
xh VARCHAR(255)  COMMENT '学号',
xm VARCHAR(255)  COMMENT '姓名',
bj VARCHAR(255)  COMMENT '班级',
zy VARCHAR(255)  COMMENT'专业',
yx VARCHAR(255)  COMMENT '院系',
xb VARCHAR(255)  COMMENT '性别',
csrq datetime COMMENT '出生日期',
rxsj datetime  COMMENT'入学时间',
mz VARCHAR(255) COMMENT '民族'
);
​
#学生成绩表
CREATE table data_xscj(
id varchar(32) primary key COMMENT '主键',
xsid varchar(255) COMMENT '学生主键',
xk varchar(255) COMMENT '学科',
cj varchar(255) COMMENT '学科成绩',
lrsj datetime COMMENT '录入时间'
); 
​
#评奖申请表
create table data_pjsq(
id varchar(32) primary key comment '主键',
sqjx VARCHAR(255) comment '申请奖项' ,
xsid VARCHAR(255) comment '学生主键' ,
sqsj datetime comment '申请时间' ,
shzt VARCHAR(4) comment '审核状态 100' 
);
​
#评奖公示表
create table data_pjgs(
id varchar(32) primary key COMMENT '主键',
sqid varchar(255) COMMENT '申请主键',
gsbz varchar(255) COMMENT '公示备注',
gssj datetime COMMENT '申请时间'
);
​
#评奖质疑表
CREATE TABLE data_pjzy(
id VARCHAR(32) PRIMARY KEY COMMENT '主键',
gsid varchar(255) COMMENT '公示主键',
zysm LONGTEXT COMMENT '质疑说明',
zysj datetime COMMENT '质疑时间'
);

项目结构图

 

实施步骤

1.连接数据库

应用程序如果直接获取连接,则每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长,这样效率不高,因此采用连接池。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。从而大大提高对数据库操作的性能。

package com.huawei.utils;
​
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.log4j.Logger;
import javax.sql.*;
import java.sql.*;
import java.util.Properties;
​
public class DataBasicSource  {
 
    private DataSource ds=null;
    static Logger logger=Logger.getLogger(DataBasicSource.class);
    public DataBasicSource() {
    }
    /**
     * 获取数据源
     * @param properties
     */
    public  DataBasicSource(String properties) {
        Properties pro=new Properties();
        try {
            pro.load(DataBasicSource.class.getClassLoader().getResourceAsStream(properties));
            //从工具包里获得Druidp提供的数据源 
            DruidDataSource dds = new DruidDataSource();
            dds.configFromPropety(pro);
            ds = dds;
            logger.info("加载属性文件:"+properties+"成功!");
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
    }
    /**
     * 获取连接
     * @return Connection
     * @throws SQLException
     */
    public Connection getConnection() throws SQLException{
        return ds.getConnection();
    }
    
    public void release(Connection conn,Statement ps,ResultSet rs) {
        try {
            if(rs!=null) {
                rs.close();
                rs=null;
            }
            if(ps!=null) {
                ps.close();
                ps=null;
            }
            if(conn!=null) {
                conn.close();
                conn=null;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } 
    }
}

2.数据库连接的子类

由于本项目中要对两个数据库进行操作,因此可以建立一个抽象类作为数据库连接的父类;学工数据源和OA数据源继承该类,然后传入对应的属性文件即可获取相应的数据源,从而获取连接。

 

 

3.读取配置文件

当我们更换数据库,或者数据库参数信息被修改时,我们就需要修改源代码来重新获取连接。更方便的方法是:把数据库的参数信息存放在属性文件中,这样,只要我们修改文件的信息,就可以不用在源代码中进行修改了。这样做的好处就是: ①不用对源代码进行修改,提高了开发效率 ②直接修改配置信息即可,这也体现了代码的可扩展性

druid.driverClassName=com.mysql.jdbc.Driver
druid.url=jdbc:mysql://127.0.0.1:3306/oa_db?characterEncoding=utf8
druid.username=root
druid.password=123456
druid.maxActive=8
druid.maxWait=5000

4.数据库同步方法

比较两个map类型的链表中的值是否相同,并保存不同的部分,供更新数据库

package com.huawei.utils;
​
import java.util.*;
​
public class ListComparatorUtil {
​
    /**
     * @param lst1 来源数据
     * @param lst2  本数据源
     * @return  List[], 数据结构说明:[新增数据集合(ArrayList<Map>),更新数据集合(ArrayList<Map>)]
     */
 public static final List[] compare(List<Map> lst1, List<Map> lst2) {
        List[] result = new List[2];
        
        ArrayList<Map> add_result = new ArrayList<Map>();//add
        result[0] = add_result;
 
        ArrayList<Map> update_result = new ArrayList<Map>();//update
        result[1] = update_result;
        
        if(lst1 != null && !lst1.isEmpty()
                && lst2 != null && !lst2.isEmpty()){
            //将List<Map>转化为Map<String,Map>
            Map<String,Map> map1 = new HashMap<String,Map>();
            for(Map m1:lst1){
                String id = m1.get("id").toString();
                map1.put(id, m1);
            }
            
            Map<String,Map> map2 = new HashMap<String,Map>();
            for(Map m1:lst2){
                String id = m1.get("id").toString();
                map2.put(id, m1);
            }
            //map1和map2对比
            Set<String> keySet = map1.keySet();
            for (String key : keySet) {
                Map m1 = map1.get(key);
                if(map2.containsKey(key)){//存在数据
                    Map m2 = map2.get(key);
                    Set m_keySet = m1.keySet();
                    boolean isSame = true;
                    for (Object m_key : m_keySet) {
                        String o1 = m1.get(m_key)+"";
                        String o2 = m2.get(m_key)+"";
                        if(o1.compareTo(o2) != 0){
                            isSame = false;
                            break;
                        }
                    }
                    if(!isSame){
                        update_result.add(m1);
                    }
                    System.out.println("m1:"+m1);
                }else{//不存在数据,该数据属于新增数据?
                    add_result.add(m1);
                }
            }
        }else{
            if(lst1 != null && !lst1.isEmpty()){
                add_result.addAll(lst1);
            }
            if(lst2 != null && !lst2.isEmpty()){//不合理情况?
            }
        }   
        return result;
    }
}

数据库同步需要首先需要通过查询返回一个链表,然后比较两个链表的内容,是要进行更新操作还是插入操作,根据比较方法返回的list<map>拼接生成我们需要的sql语句,调用之前封装好的更新方法实现同步。 这里需要注意比较链表信息调用的compare方法中,传递的参数是有顺序的,前面一个参数是被比较的参数,后者是需要进行比较的参数,例如,oa数据库同步xg数据库线程,oa数据库中的学生信息表与xg数据库中的学生信息作比较,第一个参数就应该是oalist;否则就成了xg数据库同步oa数据了。 在这里还可能遇到的问题是,oalist和xglist查出来的数据是一样的(数据库里面是不一样的),原因很可能就是数据库连接出了问题,用的是一个数据库连接,而导致这个的原因是,把数据库连接方法声明成了静态的,就不会每次都从连接池获取一个新的连接了。当然,还有很多别的原因也可能导致这个问题

package com.huawei.utils;
​
import org.apache.log4j.Logger;
import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
​
public class SqlOption {
​
    static Logger logger = Logger.getLogger(DataBasicSource.class);/*实例化日志类*/
​
    /**
     * 封装preparedStatement,用map装参数,将参数(:id)替换回‘?’,再通过preparedStatement的set方法用实际值替换掉‘?’
     * @param conn
     * @param sql
     * @param params
     * @return
     * @throws SQLException
     */
    public PreparedStatement prepareStatement(Connection conn, String sql, Map<String, Object> params) throws SQLException {
        PreparedStatement pstat = null;
        List<Object> paramList = new ArrayList<Object>();// 参数列表集合
        List<String> keyList = new ArrayList<String>();//:后面的值
        StringBuffer sqlbak = new StringBuffer();//作为加入:xx为分割后的字符串的容器
        if (params != null && !params.isEmpty()) {
            String para = isPlaceholder(sql, keyList);//判断占位符是否正确
            String[] split = sql.split(para);
            int length = split.length;
            boolean split_last_part_is_space = "".equals(split[length - 1].trim());// sql语句分隔之后的最后部分是否为空白符
            for (int i = 0; i < length; i++) {
                int keyListSize = keyList.size();
                sqlbak.append(split[i]);
                if ((split_last_part_is_space && i == length - 1) || i == keyListSize) {// 当最后部分为空白符时,最后部分就不存在参数占位符
                    continue;
                }
                String key = keyList.get(i);
                appendstr(params, paramList, sqlbak, key);
            }
            logger.info("预编译语句:" + sqlbak);
            pstat = pstat_set(conn, pstat, paramList, sqlbak);
        } else {
            pstat = conn.prepareStatement(sql);//没有参数的情况就直接执行预编译
            logger.info(sql);
        }
        return pstat;
    }
​
    /**
     * 给preparestatement?替换为params(map)的值,预编译结束返回pstat
     * select * form data_xsxx where id ='x001'
     * @param conn
     * @param pstat
     * @param paramList
     * @param sqlbak
     * @return
     * @throws SQLException
     */
    private PreparedStatement pstat_set(Connection conn, PreparedStatement pstat, List<Object> paramList,
                                        StringBuffer sqlbak) throws SQLException {
        if (!paramList.isEmpty()) {
            pstat = conn.prepareStatement(sqlbak.toString(), ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_READ_ONLY);
            for (int i = 0; i < paramList.size(); i++) {
                Object value = paramList.get(i);
                if (value instanceof java.util.Date) {// 日期格式转换
                    java.util.Date dateValue = (java.util.Date) value;
                    java.sql.Timestamp timestamp = new Timestamp(dateValue.getTime());
                    pstat.setObject(i + 1, timestamp);
                } else {
                    pstat.setObject(i + 1, value);//preparestatement将?设成map里面的参数的值
                }
            }
        }
        return pstat;
    }
​
    /**
     * sqlbak字符串 添加占位符 ?   并将params的值添加到paramList
     * select * form data_xsxx where id =?  paramlist->[x001]
     *
     * @param params
     * @param paramList
     * @param sqlbak
     * @param key
     */
    private void appendstr(Map<String, Object> params, List<Object> paramList, StringBuffer sqlbak, String key) {
        if (params.containsKey(key)) {
            Object object = params.get(key);//参数的值
            if (object instanceof Collection) {// 集合参数
                Collection<?> c = (Collection<?>) object;
                Iterator<?> iterator = c.iterator();
                StringBuffer part = new StringBuffer();
                if (iterator.hasNext()) {
                    Object obj = iterator.next();
                    paramList.add(obj);
                    part.append(",?");
                }
                sqlbak.append(part.substring(1));
            } else if (object instanceof Object[]) {// 数组参数
                Object[] objs = (Object[]) object;
                StringBuffer part = new StringBuffer();
                for (Object obj : objs) {
                    paramList.add(obj);
                    part.append(",?");
                }
                sqlbak.append(part.substring(1));
            } else {// 单个参数
                paramList.add(object);
                sqlbak.append("?");
            }
        } else {
            logger.info("参数params未找到匹配的占位符变量");
        }
    }
​
    /**
     * 判断是否是正确的占位符形式(:id)并找到该占位符id 放入keylist
     *
     * @param sql
     * @param keyList
     * @return
     */
    private String isPlaceholder(String sql, List<String> keyList) {
        String para = ":[[A-Za-z_0-9]]+";
        Pattern p = Pattern.compile(para);
        Matcher m = p.matcher(sql);
​
        while (m.find()) {
            int index = m.start();
            int end = m.end();
            String key = sql.substring(index + 1, end);
            keyList.add(key);
        }
        return para;
    }
​
    /**
     * 分页查询数据库信息
     *
     * @param sql
     * @param params map形式的参数列表
     * @param m      从第m条数据开始
     * @param n      到底n条结束
     * @return List<Map < String, Object>>
     * @throws SQLException
     */
    public List<Map> queryForList(DataBasicSource ds, String sql, Map params, int m, int n) throws SQLException {
        List<Map> result = new ArrayList<Map>();
        Connection conn = ds.getConnection();
        PreparedStatement pstat = null;
        ResultSet rs = null;
        try {
            pstat = prepareStatement(conn, sql, params);
            pstat.setMaxRows(m + n - 1);//版本问题
            rs = pstat.executeQuery();
            if (rs != null) {
                ResultSetMetaData metaData = rs.getMetaData();
                int columnCount = metaData.getColumnCount();
                if (m - 1 > 0) {
                    rs.absolute(m - 1);
                }
                while (rs.next()) {
                    Map<String, Object> map = new LinkedHashMap<String, Object>();
                    for (int i = 0; i < columnCount; i++) {
                        String columnName = metaData.getColumnName(i + 1);
                        map.put(columnName, rs.getObject(i + 1));
                    }
                    result.add(map);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            ds.release(conn, pstat, null);
        }
        return result;
    }
​
    /**
     * 更新数据库信息
     *
     * @param sql
     * @param params
     * @return 影响的行数
     */
    public int executeUpdate(DataBasicSource ds, String sql, Map params) {
        int result = 0;
        Connection conn = null;
        PreparedStatement pstat = null;
​
        try {
            conn = ds.getConnection();
            pstat = prepareStatement(conn, sql, params);
            result = pstat.executeUpdate();// 真正执行SQL语句的处理代码
​
        } catch (Exception e) {
            System.err.println(e.getMessage());
        } finally {
            ds.release(conn, pstat, null);
        }
        logger.info("更新影响的条数:" + result);
        return result;
    }
}

5.观察者模式使用

本项目中,oa数据库可以xg同步学工数据库,xg学工数据库也可以同步oa数据库,所以选择使用观察者模式观察数据库的SQL日志变化,一但发现日志有变动 则触发相应的操作

数据库日志是被观察的对象

 

读取日志工具类

package com.huawei.utils;
​
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
 * 读取日志
 */
public class SeeLog {
    private final String FILEPATH="D:\\data\\localhost.log";
    private static String filename;
    File file=new File(FILEPATH+filename);
    public SeeLog(String filename) {
        this.filename = filename;
        File fileDir= new File(FILEPATH);
        file=new File(FILEPATH+filename);
        if(!fileDir.exists()){
            fileDir.mkdirs();
        }
        if(!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
​
    public String read() {
        String fileString="";
        String lineString="";
        try {
            File  file=new File(FILEPATH+filename);
            FileReader fr =new FileReader(file);
            BufferedReader br=new BufferedReader(fr);
            while((lineString=br.readLine())!=null){
                fileString+=lineString+"\n";
            }
            fr.close();
            br.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fileString;
    }
    
    public void write(String data, boolean mode) {
        try {
            File  file=new File(FILEPATH+filename);
            FileWriter fw=new FileWriter(file,mode);
            fw.write(data);
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Win10中设置Mysql日志

在 MySQL的安装目录下的 my.ini 配置文件的 [mysqld] 下增加以下信息,日志路径可修改。

[mysqld]
#开启日志
log_bin=D:/Program Files/mysql-5.7.23/logs/mysql-bin.log
log_error=D:/Program Files/mysql-5.7.23/logs/mysql.err
long_query_time=0.1
slow_query_log=ON
slow_query_log_file=D:/Program Files/mysql-5.7.23/logs/mysql-slow.log
general_log=ON
general_log_file=D:/Program Files/mysql-5.7.23/logs/logmysql.log
server-id=1

mysql常用的有5种日志,分别为:

1、普通日志(general_log),任何执行的sql语句都会写入这个日志中。

2、慢查询日志(slow_log),记录超过“慢查询时间”的语句。

3、二进制日志(bin_log),记录所有更改数据的语句,可用于数据拷贝。

4、错误日志(error_log),mysql启动、停止、运行过程中的报错信息。

5、中继日志(relay_log),主从复制的日志。

提示:我们这里一般检测普通日志即可,按照常理两个数据库,相应有两个被观察者读取两个不同数据库的日志文件。

6.数据同步实现

由于本项目中,oa数据库可以xg同步学工数据库,xg学工数据库也可以同步oa数据库,所以选择建两个线程类去执行我们的同步数据库代码。分别是oa同步xg学工线程;xg学工同步oa线程。因为同步数据库需要数据源以查询相关的数据库信息,然后通过得到的信息进行同步。因此,我们的线程需要传我们的数据源进行初始化。两个线程唯一不同的就是一个是以oa数据库作为更新模板,另一个是以oa数据库作为需要同步的那个数据库。可以建一个父类,让这两个线程继承该类。

编写数据同步抽象父类

实现观察者接口,由子类实现其update方法

package com.huawei.transfer;
​
import com.huawei.utils.DataBasicSource;
import com.huawei.utils.OADataSource;
import com.huawei.utils.SqlOption;
import com.huawei.utils.XGDataSource;
import java.util.*;
​
/**
 * 数据同步抽象类
 */
public abstract class TransferObserver implements Observer {
    protected OADataSource oa;
    protected XGDataSource xg;
​
    public TransferObserver(OADataSource oaDataSource, XGDataSource xgDataSource) {
        this.oa = oaDataSource;
        this.xg = xgDataSource;
    }
​
    /**
     * 生成sql并执行
     * @param tableName 表名
     * @param compare 比较完成的差异信息
     * @param ds 目标数据源
     */
    public void generateSql(String tableName, List[] compare, DataBasicSource ds) {
        List<Map> add_result = compare[0];// add
        System.out.println(add_result);
        if (add_result != null && !add_result.isEmpty()) {
            //INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....)
            for (int i = 0; i < add_result.size(); i++) {
                Map map = add_result.get(i);
                Set keySet = map.keySet();
                Map<String, Object> params = new HashMap<String, Object>();
                StringBuffer sql = new StringBuffer();//"insert into data_xsxx() VALUES()";
                StringBuffer flieds = new StringBuffer();
                StringBuffer values = new StringBuffer();
                for (Object key : keySet) {
                    flieds.append(",").append(key);
                    values.append(",:").append(key);
                    params.put(key.toString(), map.get(key));
                }
                sql.append("insert into ").append(tableName).append("(").append(flieds.deleteCharAt(0)).append(")")
                        .append(" VALUES(").append(values.deleteCharAt(0)).append(")");
                System.out.println(sql);
                new SqlOption().executeUpdate(ds, sql.toString(), params);
            }
        }
​
        List<Map> update_result = compare[1];// update
        System.out.println(update_result);
        if (update_result != null && !update_result.isEmpty()) {
            for (int i = 0; i < update_result.size(); i++) {
                Map map = update_result.get(i);
                Set keySet = map.keySet();
                Map<String, Object> params = new HashMap<String, Object>();
                StringBuffer sql = new StringBuffer();// "insert into data_xsxx() VALUES()";
                StringBuffer flieds = new StringBuffer();
                for (Object key : keySet) {
                    flieds.append(",").append(key).append("=:").append(key);
                    params.put(key.toString(), map.get(key));
                }
                sql.append("UPDATE ").append(tableName).append(" set ").append(flieds.deleteCharAt(0))
                        .append(" WHERE id=:id");
                System.out.println(sql);
                new SqlOption().executeUpdate(ds, sql.toString(), params);
            }
        }
    }
}

两个子类实现

OAtoXGObserver

XGtoOAObserver

主程序类

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乐韬科技

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值