做自己的ORM,不将就,就是挑剔!
写在前面
一直以来都对各种数据库的ORM框架抱以将就的心态,用起来麻烦不顺手,于是我就手动做了一个,并写下这篇文章。
轻松的阅读本文你需要:
- 有使用ORM框架的经验,比如Hibernate、Mybatis等。
- 熟悉commons-dbutils工具类。
- 了解Java反射技术。
- 一颗对技术不将就、有追求的心。
一般的ORM都有什么?
拿最出名的Hibernate举例子,最方便的地方就是可以直接通过实体进行更新、删除、新增等操作;查询完成后会自动转换为实体;对于hql和sql那个更好,个人觉得sql更好,因为不用在写完sql测试完成后再手动转换为hql;对于实体关联,连表查询结果使用框架转换为实体,个人是不喜欢,因为有更方便更高效的做法。
从改造dbutils开始
Apache的开源项目commons-dbutils提供了一些简单的方法,帮助我们完成数据库与程序的交互。其中最为重要的就是,把数据库的返回结果转换成实体对象。
但是这个方法比较基础,默认数据库的列名要与实体的字段名一致。而我们实际的情况一般是,数据库的列名是user_name、实体的字段名是userName,为了让dbutils转换实体的时候遵守这种约定,需要对dbutils进行改造。
public class CustomBeanProcessor extends BeanProcessor{
@Override
protected int[] mapColumnsToProperties(ResultSetMetaData rsmd, PropertyDescriptor[] props) throws SQLException {
int cols = rsmd.getColumnCount();
int[] columnToProperty = new int[cols + 1];
Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
for (int col = 1; col <= cols; col++) {
String columnName = rsmd.getColumnLabel(col);
if (null == columnName || 0 == columnName.length()) {
columnName = rsmd.getColumnName(col);
}
String propertyName = SqlHelper.camelConvertColumnName(columnName); // 只需要修改这一行代码
if (propertyName == null) {
propertyName = columnName;
}
for (int i = 0; i < props.length; i++) {
if (propertyName.equalsIgnoreCase(props[i].getName())) {
columnToProperty[col] = i;
break;
}
}
}
return columnToProperty;
}
}
新建上面的类,继承自dbutils的BeanProcessor,重写mapColumnsToProperties方法,代码完全拷贝,只需要修改上面加注释的一行代码,功能类似把字符串user_name转换成userName,第一步完成。
public class CustomBasicRowProcessor extends BasicRowProcessor{
public CustomBasicRowProcessor() {
super(new CustomBeanProcessor());
}
}
新建上面的类,继承自dbutils的BasicRowProcessor,没有其他的方法,只是在初始化的时候使用我们自己创建的CustomBeanProcessor,到此dbutils改造完成。
数据库连接
对数据库所有操作都是从获取数据库链接开始的,一般叫做Connection或者Session。而获取链接之前你需要先配置数据库连接,一般需要的几个必要条件是 数据库的地址、用户名、密码,这里暂时使用MysqlDataSource进行配置链接。
private MysqlDataSource getDataSource(){
MysqlDataSource dataSource=new MysqlDataSource();
try {
dataSource.setURL("jdbc:mysql://127.0.01:3306/test");
dataSource.setUser("admin");
dataSource.setPassword("password");
dataSource.setCharacterEncoding("utf-8");
dataSource.setConnectTimeout(30000);
} catch (Exception e) {
e.printStackTrace();
}
return dataSource;
}
有了数据库配置之后就可以获取数据库连接。
public Connection getConnection() throws Exception{
return dataSource.getConnection();
}
当然还有关闭数据库连接,开启事务,回滚事务等。
public void close(Connection connection){
try {
DbUtils.close(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void rollback(Connection connection){
try {
DbUtils.rollback(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
// 开启事务 connection.setAutoCommit(false);
查询和更新
新增、更新和删除对数据库来说都是更新操作,所以这里只提供了两个方法,新增返回插入数据库的id,更新和删除返回受影响的行数。
private final QueryRunner queryRunner=new QueryRunner();
public int executeUpdate(String sql,List<?> params,Connection connection,boolean rowId,boolean close) throws Exception{
try {
PreparedStatement pstm=connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
int index=1;
for (Object object : params) {
pstm.setObject(index++, object);
}
int effectCount=pstm.executeUpdate();
if(rowId){
ResultSet rs=pstm.getGeneratedKeys();
if(rs.next()) return rs.getInt(1);
}
else return effectCount;
return -1;
} catch (Exception e) {
throw e;
} finally {
if (close) close(connection);
}
}
public <T> T executeQuery(String sql,List<?> params,ResultSetHandler<T> handler, Connection conn, boolean close) throws Exception{
try {
return queryRunner.query(conn, sql, handler, params.toArray());
} catch (Exception e) {
throw e;
} finally {
if (close) close(conn);
}
}
到这里我们完成了基础的功能,已经可以获取数据库连接、执行简单的sql了。
像ORM那样去根据实体操作数据库
前面说到Hibernate可以根据实体去完成新增,更新和删除操作,那具体是怎么做到的呢?当然万变不离其宗,依然是通过sql进行数据库的交互。通过前面做的事情,我们已经可以跑sql了,那么剩下的问题就是,怎么通过实体生成sql语句?Java反射。
生成新增sql
遍历实体的所有字段,得到实体的名字和值,自动跳过值为null的字段,int、double等基本数据类型默认都是有值的,不会跳过,我的做法是不使用基本数据类型,使用Integer、Double等的封装数据类型。
public <T> SqlValue createSaveSql(T entity) throws Exception {
Class<?> entityClass = entity.getClass();
StringBuilder builder = new StringBuilder("insert into ");
String tableName=camelConvertFieldName(entityClass.getSimpleName());
builder.append(tableName).append(" ( ");
List<Object> values = new ArrayList<Object>();
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
String key = camelConvertFieldName(field.getName());
field.setAccessible(true);
Object value = field.get(entity);
if (value==null) continue;
builder.append(key).append(" , ");
values.add(value);
}
if (values.size()<1) return null;
builder.delete(builder.lastIndexOf(" , "), builder.length());
builder.append(" ) values ( ");
for (int i = 0; i < values.size(); i++) {
builder.append("? , ");
}
builder.delete(builder.lastIndexOf(" , "), builder.length());
builder.append(" )");
String sql=builder.toString();
return new SqlValue(sql, values);
}
生成更新sql
默认设定id作为where条件,其他值不为null的字段作为要更新的字段。当然这里自定义了一个@Id的注解,也可以使用第三方的ORM注解。
public <T> SqlValue createUpdateSql(T entity) throws Exception{
Class<?> entityClass = entity.getClass();
StringBuilder builder = new StringBuilder("update ");
String tableName=camelConvertFieldName(entityClass.getSimpleName());
builder.append(tableName).append(" set ");
String idFieldName=null;
Object idFieldValue=null;
Field[] fields = entityClass.getDeclaredFields();
List<Object> values = new ArrayList<Object>();
for (Field field : fields) {
String key = camelConvertFieldName(field.getName());
field.setAccessible(true);
Object value = field.get(entity);
if (value==null) continue;
if (field.isAnnotationPresent(Id.class)) { // 自定义@Id注解
idFieldName=key;
idFieldValue=value;
continue;
}
builder.append(key).append(" = ? , ");
values.add(value);
}
if (values.size()<1) return null;
builder.delete(builder.lastIndexOf(" , "), builder.length());
if (idFieldName!=null&&idFieldValue!=null) {
builder.append(" where ").append(camelConvertFieldName(idFieldName)).append(" = ? ");
}
values.add(idFieldValue);
String sql=builder.toString();
return new SqlValue(sql, values);
}
生成删除sql
实体所有的不为null的字段都作为where条件,一般只传一个id字段。
public <T> SqlValue createDeleteSql(T entity) throws Exception {
Class<?> entityClass = entity.getClass();
StringBuilder builder = new StringBuilder("delete from ");
String tableName=camelConvertFieldName(entityClass.getSimpleName());
builder.append(tableName).append(" where ");
Field[] fields = entityClass.getDeclaredFields();
List<Object> values = new ArrayList<Object>();
for (Field field : fields) {
String key = camelConvertFieldName(field.getName());
field.setAccessible(true);
Object value = field.get(entity);
if (value==null) continue;
builder.append(key).append(" = ? and ");
values.add(value);
}
if (values.size()<1) return null;
builder.delete(builder.lastIndexOf(" and "), builder.length());
String sql=builder.toString();
return new SqlValue(sql, values);
}
接收实体
我们已经可以根据实体生成sql语句了,接下来把数据库连接,执行sql语句的方法联系起来。
public <T> int save(T entity) throws Exception{
SqlValue sv=queryStringHelper.createSaveSql(entity);
Connection connection=getConnection();
return executeUpdate(sv.getSql(), sv.getValues(), connection,true, true);
}
public <T> int update(T entity) throws Exception{
SqlValue sv=queryStringHelper.createUpdateSql(entity);
Connection connection=getConnection();
return executeUpdate(sv.getSql(), sv.getValues(), connection,false, true);
}
public <T> int delete(T entity) throws Exception{
SqlValue sv=queryStringHelper.createDeleteSql(entity);
Connection connection=getConnection();
return executeUpdate(sv.getSql(), sv.getValues(), connection,false, true);
}
传递对象SqlValue的结构如下:
public class SqlValue {
private String sql;
private List<Object> values;
}
让查询来的更简单一点吧
上面的executeQuery方法需要提供一个参数ResultSetHandler<T> handler,这个是dbutils的query方法要求传递的对象,用处是把返回结果转换成实体对象。
private final CustomBasicRowProcessor rowProcessor=new CustomBasicRowProcessor();
public <T> List<T> getList(String sql,List<?> params) throws Exception{
Connection connection=getConnection();
Class<T> entityClass=queryStringHelper.getClassFromSql(sql);
return executeQuery(sql, params, new BeanListHandler<T>(entityClass, rowProcessor), connection, true);
}
public <T> T getOne(String sql,List<?> params) throws Exception{
Class<T> entityClass=queryStringHelper.getClassFromSql(sql);
Connection connection=getConnection();
return executeQuery(sql, params, new BeanHandler<T>(entityClass, rowProcessor), connection, true);
}
public <T> T getById(String sql,int id) throws Exception{
Class<T> entityClass=queryStringHelper.getClassFromSql(sql);
List<Object> params=new ArrayList<Object>();
params.add(id);
Connection connection=getConnection();
return executeQuery(sql, params, new BeanHandler<T>(entityClass, rowProcessor), connection, true);
}
public Long getLong(String sql,List<?> params) throws Exception{
Connection connection=getConnection();
return executeQuery(sql, params, new ScalarHandler<Long>(), connection, true);
}
加入c3p0连接池
有连接池毕竟是好的,能提升整个框架的相应速度,用ComboPooledDataSource替换之前的MysqlDataSource。
private final ComboPooledDataSource dataSource=getDataSource();
private ComboPooledDataSource getDataSource(){
ComboPooledDataSource pooledDataSource=new ComboPooledDataSource();
pooledDataSource.setUser("username");
pooledDataSource.setPassword("password");
pooledDataSource.setJdbcUrl("url");
try {
pooledDataSource.setDriverClass("com.mysql.jdbc.Driver");
} catch (Exception e) {
e.printStackTrace();
}
pooledDataSource.setInitialPoolSize(3);
pooledDataSource.setMinPoolSize(3);
pooledDataSource.setMaxPoolSize(10);
pooledDataSource.setMaxIdleTime(60);
pooledDataSource.setMaxStatements(50);
return pooledDataSource;
}
实体
一般的ORM框架都要求一套严谨的实体配置文件,好一点的可以用注解配置,顺便带上各种插件,让实体根据数据库结构自动生成。我使用的是OpenJPA插件,这个插件Eclipse本身就自带,没有复杂的配置文件,配置使用注解实现。
而上面做的这套框架,无视你的配置文件(除了一个@Id注解),你甚至建一个普通的JavaBean也是可行的。
结语
对于缓存,我觉得并没有什么大的必要,因为应用层的缓存粒度比ORM框架层的缓存粒度相对要细的多,所以这里并不加入缓存机制。
github地址: /leeyaf/orm