初级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操作,操作完成后再恢复表名和数据源。