架构师内功心法-----基于Spring JDBC手写定制自己的ORM框架


初级orm

jdbc执行查询

    private static List<Member> select(String sql) {
        List<Member> result = new ArrayList<>();
        Connection con = null;          //连接对象
        PreparedStatement pstm = null;  //语句集
        ResultSet rs = null;            //结果集
        try {
            //1、加载驱动类,千万不要忘记了
            Class.forName("com.mysql.jdbc.Driver");
            //2、建立连接
            con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java","root","123456");
            //3、创建语句集
            pstm =  con.prepareStatement(sql);
            //4、执行语句集
            rs = pstm.executeQuery();
            while (rs.next()){
                //纯粹的硬编码
                Member instance = new Member();
                instance.setId(rs.getLong("id"));
                instance.setName(rs.getString("name"));
                instance.setAge(rs.getInt("age"));
                instance.setAddr(rs.getString("addr"));
                result.add(instance);
            }
            //5、获取结果集
        }catch (Exception e){
            e.printStackTrace();
        }
        //6、关闭结果集、关闭语句集、关闭连接
        finally {
            try {
                rs.close();
                pstm.close();
                con.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return result;
    }

动态拼接sql,自动解析结果集======拼接sql时根据属性设置字段值,解析结果集时根据字段值设置属性

@Entity
@Table(name="t_member")
@Data
public class Member implements Serializable {
    @Id private Long id;
    private String name;
    private String addr;
    private Integer age;

    public Member() {
    }

    public Member(String name, String addr, Integer age) {
        this.name = name;
        this.addr = addr;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", addr='" + addr + '\'' +
                ", age=" + age +
                '}';
    }
}
public static void main(String[] args) {
       
        Member condition = new Member();
        condition.setName("TomCat");
        condition.setAge(2);

        //"select * from t_member where name = 'TomCat' and age = 2"
        List<?> result = select(condition );
        System.out.println(JSON.toJSONString(result,true));
    }


    public static List<?> select(Object condition) {

        List<Object> result = new ArrayList<>();

        Class<?> entityClass = condition.getClass();

        Connection con = null;          //连接对象
        PreparedStatement pstm = null;  //语句集
        ResultSet rs = null;            //结果集

        try {
            //1、加载驱动类,千万不要忘记了
            Class.forName("com.mysql.jdbc.Driver");
            //2、建立连接
            con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/how2java","root","123456");

            //用到反射
            //无反射,不框架
            //无正则,不架构
            Map<String,String> getFieldNameByColumn = new HashMap<String,String>();
            Map<String,String> getColumnByFieldName = new HashMap<String,String>();
            Field[] fields = entityClass.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                String fieldName = field.getName();
                if(field.isAnnotationPresent(Column.class)){
                    Column column = field.getAnnotation(Column.class);
                    //别名优先
                    String columnName = column.name();
                    getFieldNameByColumn.put(columnName,fieldName);
                    getColumnByFieldName.put(fieldName,columnName);
                }else{
                    //默认属性名就是列名
                    getFieldNameByColumn.put(fieldName,fieldName);
                    getColumnByFieldName.put(fieldName,fieldName);
                }
            }


            StringBuffer sql = new StringBuffer();
            //3、创建语句集
            Table table = entityClass.getAnnotation(Table.class);
            sql.append("select * from " + table.name() + " where 1=1 ");
            for (Field field : fields) {

                Object value = field.get(condition);
                if(null != value){
                    if(String.class == field.getType()){
                        sql.append(" and " + getColumnByFieldName.get(field.getName()) + " = '" + value + "'");
                    }else{
                        sql.append(" and " + getColumnByFieldName.get(field.getName()) + " = " + value);
                    }
                    //其他依次类推
                }
            }

           pstm = con.prepareStatement(sql.toString());

            //4、执行,获取结果集
           rs = pstm.executeQuery();

           //MetaDate 元信息
           int columnCounts = rs.getMetaData().getColumnCount();
           while (rs.next()){
               //一行一行往下读数据
               Object instance = entityClass.newInstance();  //反射

               for (int i = 1; i <= columnCounts; i++) {
                   String columnName = rs.getMetaData().getColumnName(i); //拿到列名

                   Field field = entityClass.getDeclaredField(getFieldNameByColumn.get(columnName));
                   field.setAccessible(true);//强吻


                   field.set(instance,rs.getObject(columnName));

               }
               result.add(instance);
           }


        }catch (Exception e){
            e.printStackTrace();
        }
        //6、关闭结果集、关闭语句集、关闭连接
        finally {
            try {
                rs.close();
                pstm.close();
                con.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return result;
    }

基于jdbcTemplate的orm

很明显,上述使用实体类对象作为入参。为了分离mysql的具体sql编写,通过QueryRule完成条件拼接。
类名及类作用如下:
QueryRule 链式构建规则
QueryRuleSqlBuilder 根据规则解析成where段sql、order段sql、以及占位符对应的值。
PropertyMapping 列名对应的属性特性,包括属性的getter、setter方法以及属性,用来获取属性的值或者设置属性的值
ClassMappings 获取实体类公有getter、setter方法。
EntityOperation 实体类操作类,包括获取实体类的所有getter、setter方法,设置主键名,设置表名、设置rowMapper映射方法、解析实体类为map、设置表所有列。
BaseDao 基本接口
BaseDaoSupport 接口实现类 使用jdbcTemplate完成增删改查
XXXDao 实体Dao,继承BaseDaoSupport,需要设置主键列和数据源

log4j日志

log4j提供了将日志发送到邮件和存入数据库的功能

### set log levels ###
##日志级别,logger名
log4j.rootLogger = trace , stdout , dailyLog , error , file , email , database

### 设置输出sql的级别,其中logger后面的内容全部为jar包中所包含的包名 ###
log4j.logger.org.apache=debug
log4j.logger.java.sql.Connection=debug
log4j.logger.java.sql.Statement=debug
log4j.logger.java.sql.PreparedStatement=debug
log4j.logger.java.sql.ResultSet=debug

### stdout ###
##输出日志目的地
#org.apache.log4j.ConsoleAppender(控制台)
#org.apache.log4j.FileAppender(文件)
#org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
#org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
#org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
#默认情况下是:System.out标准输出流 可选值:System.err标准错误输出流
log4j.appender.stdout.Target = System.out
##输出日志布局
#org.apache.log4j.HTMLLayout(以HTML表格形式布局)
#org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
#org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
#org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %-d{yyyy-MM-dd HH\:mm\:ss} [%p]-[%c] %m%n
#默认值是true,意味着所有的消息都会被立即输出。
log4j.appender.stdout.ImmediateFlush=true
#日志输出的最低级别
log4j.appender.stdout.Threshold=debug

### 每天一个 ###
log4j.appender.dailyLog = org.apache.log4j.DailyRollingFileAppender
#指定文件输出位置
log4j.appender.dailyLog.File = ../logs/dailyLog.log
#默认通过文件流写入,此处使用缓存流
#log4j.appender.dailyLog.bufferedIO=true
#缓存流缓存大小
#log4j.appender.dailyLog.bufferSize=1
#内容是否追加写入,false则覆盖
log4j.appender.dailyLog.Append = true
log4j.appender.dailyLog.Threshold = DEBUG
log4j.appender.dailyLog.layout = org.apache.log4j.SimpleLayout

### 滚动文件 ###
log4j.appender.error = org.apache.log4j.RollingFileAppender
log4j.appender.error.File = ../logs/error.log
log4j.appender.error.Append = true
log4j.appender.error.Threshold = ERROR
log4j.appender.error.layout = org.apache.log4j.TTCCLayout
#后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。
log4j.appender.error.MaxFileSize=100KB
#指定可以产生的滚动文件的最大数。
log4j.appender.error.MaxBackupIndex=2

### 文件 ###
log4j.appender.file = org.apache.log4j.FileAppender
log4j.appender.file.File = ../logs/file.html
log4j.appender.file.Append = true
log4j.appender.file.layout = org.apache.log4j.HTMLLayout

### 输出到邮件 ###
log4j.appender.email=org.apache.log4j.net.SMTPAppender
log4j.appender.email.Threshold=error
log4j.appender.email.BufferSize=10
log4j.appender.email.From=xxx@163.com
log4j.appender.email.SMTPHost=smtp.163.com
log4j.appender.email.SMTPUsername=xxx@163.com
log4j.appender.email.SMTPPassword=网易163授权密码
log4j.appender.email.Subject=Log4J Message  \u6d4b\u8bd5\u90ae\u4ef6
log4j.appender.email.To=xxx@qq.com
log4j.appender.email.layout = org.apache.log4j.PatternLayout
log4j.appender.email.layout.ConversionPattern = %-d{yyyy-MM-dd HH\:mm\:ss} [%p]-[%c] %m%n

### 输出到数据库 ###
log4j.appender.database=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.database.URL=jdbc:mysql://xxx:3306/how2java?characterEncoding=UTF-8
log4j.appender.database.driver=com.mysql.jdbc.Driver
log4j.appender.database.user=root
log4j.appender.database.password=123456
log4j.appender.database.sql=INSERT INTO log(msg) VALUES ('%-d{yyyy-MM-dd HH\:mm\:ss} [%p]-[%c] %m%n')
log4j.appender.database.layout=org.apache.log4j.PatternLayout

druid连接池

提供了将密码加密,防火墙及数据源监控等功能,此处不展示防火墙及数据源监控功能。

#sysbase database mysql config
#dos进入到druid的jar包所在位置,java -cp druid-1.0.16.jar com.alibaba.druid.filter.config.ConfigTools 123456  加密密码
#原密码 123456
#公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOV2ApLwuRAQeC3KJ2ar+fmEaTtEvw0Ort4e/o/d4Yn+aiWx3UJ7aGUIH3S5FfaCMD45wV5O8UhjUNlFt4zNy7ECAwEAAQ==
#密文 C68NXvmrerunCpBGoG+CjJBT3pnulfxsqoFcFM9rsc+py86FGJu7tRkDLyxj+NR2v52xTVXWIn8+TsgaTRfjFg==

#数据源名称
default.db.name=default-dataresource
mysql.jdbc.driverClassName=com.mysql.jdbc.Driver
mysql.jdbc.url=jdbc:mysql://xxx:3306/how2java?characterEncoding=UTF-8&rewriteBatchedStatements=true
mysql.jdbc.username=root
mysql.jdbc.password=C68NXvmrerunCpBGoG+CjJBT3pnulfxsqoFcFM9rsc+py86FGJu7tRkDLyxj+NR2v52xTVXWIn8+TsgaTRfjFg==
mysql.jdbc.publickey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOV2ApLwuRAQeC3KJ2ar+fmEaTtEvw0Ort4e/o/d4Yn+aiWx3UJ7aGUIH3S5FfaCMD45wV5O8UhjUNlFt4zNy7ECAwEAAQ==


#alibaba druid config
#初始连接数
dbPool.initialSize=1
#最小空闲连接数
dbPool.minIdle=1
#最大连接数
dbPool.maxActive=20
#获取连接最大等待时间ms
dbPool.maxWait=60000
#获取连接时测试空闲连接是否可用,前置条件:testOnBorrow为false,连接空闲时间超过timeBetweenEvictionRunsMillis
dbPool.testWhileIdle=true
#判断连接是否空闲的时间阈值ms
dbPool.timeBetweenEvictionRunsMillis=60000
#连接空闲时间超过该值则移出连接池ms
dbPool.minEvictableIdleTimeMillis=300000
#测试连接是否可用(的sql)
dbPool.validationQuery=SELECT 1
#获取连接时测试连接是否可用
dbPool.testOnBorrow=false
#归还连接时测试连接是否可用
dbPool.testOnReturn=false
#是否缓存PreparedStatement(PSCache),提高支持游标的数据库的性能
dbPool.poolPreparedStatements=false
#启用PSCache,则该值需>0
dbPool.maxPoolPreparedStatementPerConnectionSize=-1
#配置扩展插件 stat:监控、log4j:日志、wall:权限拦截、config:解密密码的配置
dbPool.filters=stat,log4j,wall,config
#自定义filters 类型:List<com.alibaba.druid.filter.Filter>
#dbPool.proxyFilters.oneFilter
#dbPool.proxyFilters.twoFilter
<bean id="datasourcePool" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
		<property name="initialSize" value="${dbPool.initialSize}" />
		<property name="minIdle" value="${dbPool.minIdle}" />
		<property name="maxActive" value="${dbPool.maxActive}" />
		<property name="maxWait" value="${dbPool.maxWait}" />
        <property name="testWhileIdle" value="${dbPool.testWhileIdle}" />
        <property name="timeBetweenEvictionRunsMillis" value="${dbPool.timeBetweenEvictionRunsMillis}" />
        <property name="minEvictableIdleTimeMillis" value="${dbPool.minEvictableIdleTimeMillis}" />
        <property name="validationQuery" value="${dbPool.validationQuery}" />
		<property name="testOnBorrow" value="${dbPool.testOnBorrow}" />
		<property name="testOnReturn" value="${dbPool.testOnReturn}" />
		<property name="poolPreparedStatements" value="${dbPool.poolPreparedStatements}" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="${dbPool.maxPoolPreparedStatementPerConnectionSize}" />
		<property name="filters" value="${dbPool.filters}" />
        <property name="proxyFilters">
            <list>
                <ref bean="oneFilter" />
                <ref bean="twoFilter" />
            </list>
        </property>
    </bean>

<bean id="defaultDataSource" parent="datasourcePool">
		<property name="driverClassName" value="${mysql.jdbc.driverClassName}" />
		<property name="url" value="${mysql.jdbc.url}" />
		<property name="username" value="${mysql.jdbc.username}" />
		<property name="password" value="${mysql.jdbc.password}" />
        <property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${mysql.jdbc.publickey}" />
        <property name="name" value="${default.db.name}"></property>
    </bean>

读写分离

当单个数据库存在读多写少的情况时,读率先成为数据库性能瓶颈。主服务器进行写操作,从服务器进行读操作,BaseDaoSupport中提供两个设置数据源的方法,XXXDao中实现这两个方法即BaseDaoSupport有读jdbcTemplate和写jdbcTemplate,然后不同的方法中使用不同的jdbcTemplate即可完成读写分离。
主服务器运行io线程,从服务器运行io线程和sql线程,从服务器io线程监听主服务器binlog的位置并写入relay中继日志,sql线程读取中继日志完成数据同步,需要注意的是从服务器的io线程和sql线程都要在正常运行的情况下才能主从复制。


分库分表

通过继承AbstractRoutingDataSource完成数据源切换。

public class DynamicDataSourceEntry {
	
	// 默认数据源  
    public final static String DEFAULT_SOURCE = "default";
  
    private final static ThreadLocal<String> local = new ThreadLocal<String>();  
	
    /** 
     * 清空数据源 
     */  
    public void clear() {  
        local.remove();
    }  
    
    /** 
     * 获取当前正在使用的数据源名字
     *  
     * @return String 
     */  
    public String get() {  
         return local.get();  
    }  
  
    /** 
     * 还原指定切面的数据源 
     *  
     * @param joinPoint 
     */
    public void restore(JoinPoint join) {  
        local.set(DEFAULT_SOURCE);  
    }
    
    /**
     * 还原当前切面的数据源
     */
    public void restore() {  
        local.set(DEFAULT_SOURCE);
    }  
  
    /** 
     * 设置已知名字的数据源 
     *  
     * @param dataSource 
     */  
    public void set(String source) {  
        local.set(source); 
    }

    /**
     * 根据年份动态设置数据源
     * @param year
     */
	public void set(int year) {
		local.set("DB_" + year);
	}
}

public class DynamicDataSource extends AbstractRoutingDataSource {  


    //entry的目的,主要是用来给每个数据源打个标记
	private DynamicDataSourceEntry dataSourceEntry;  
    
    @Override  
    protected Object determineCurrentLookupKey() {
        return this.dataSourceEntry.get();  
    }  
  
    public void setDataSourceEntry(DynamicDataSourceEntry dataSourceEntry) {  
        this.dataSourceEntry = dataSourceEntry;
    }
    
    public DynamicDataSourceEntry getDataSourceEntry(){
    		return this.dataSourceEntry;
    }
}

<bean id="dynamicDataSource" class="javax.core.common.jdbc.datasource.DynamicDataSource" >
		<property name="dataSourceEntry" ref="dynamicDataSourceEntry"></property>
		<property name="targetDataSources">
			<map>
				<entry key="DB_2020" value-ref="dataSource2020"></entry>
				<entry key="DB_2021" value-ref="dataSource2021"></entry>
				<entry key="default" value-ref="defaultDataSource"></entry>
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="defaultDataSource" />
	</bean>

DynamicDataSource是通过键找数据源的,每次获取数据源连接时,方法调用顺序是getConnection --> determineTargetDataSource() --> determineCurrentLookupKey(),因此只要determineCurrentLookupKey方法中返回的值改变就能切换数据源。
DynamicDataSourceEntry中提供了一个ThreadLocal存储的字符串就是DynamicDataSource数据源的键,该类提供了设置键获取键还原键等方法。

BaseDaoSupport和EntityOperation中都存在设置和获取表名两个方法,EntityOperation存的是原表名,而BaseDaoSupport存的是切换后的表名。

当xxxDao进行操作时,先通过改变DynamicDataSourceEntry中的键切换数据源,再通过BaseDaoSupport中的方法改变表名,进行ddl和dml操作,操作完成后再恢复表名和数据源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值