前言
在Java应用开发中,凡是有数据库操作的项目都要面对ORM(数据对象映射)的问题。一个优秀的ORM框架可以省下大量的重复代码,屏蔽数据库操作的细节,让人们把主要注意力都放在业务逻辑上,从而提高开发工作的效率和质量。
常见的Java应用ORM框架有Hibernate,MyBatis等,Spring全家桶里面也有ORM解决方案。
我们今天就来尝试一下,自己动手建立一个ORM框架,建设的目标除了基本增删改查功能外,还要有分页查询、属性映射、数据缓存和查询缓存、事务管理等功能,要有广泛的适用范围,有很好的扩展性。
内容较多会分多篇文章,在最近几天陆续贴出,有不足、错误之处,欢迎大家批评指正。
实现ORM基本功能
建设目标
建设目标:
1、实现增删改查功能
2、查询有单表标准化查询和自由SQL查询两种
3、有分页查询功能
本ORM框架和主流ORM相比,有两个主要的特点:
1、主流ORM的实体类就是POJO,而在本框架下实体类要求继承自Entity;
2、主流ORM框架操作实体的对象被称为DAO(MyBatis里面是Mapper),在本框架中,DAO被分为两个层次,处理实体对象的称为EAO,用SQL处理数据的被称为DAO。
类结构图
先看看类图:
可以在这里下载本篇文章的源代码: https://download.csdn.net/download/caim/10931533
com.integ.dao包是和数据库处理相关的类,主要类包括:
- DataAccessObject -- 数据访问对象,用sql 操作数据库,需要借助apache的dbcp连接池,Spring的JdbcTemplate, RowMapper
- DAOBuilder -- DataAccessObject对象的建造器
- JdbcConfig -- Jdbc配置参数模型,属性有 driverClassName,url,userName, password,在DAOBuilder中用到
- SqlBuilder -- SQL的建造器,根据构造传入的QueryRequest对象构造各种查询SQL,适用于MySQL
- QueryRequest -- 查询请求模型,抽象类,有TabQuery,SqlQuery两个实现
- TabQuery -- 单表标准查询请求模型 继承自QueryRequest
- SqlQuery -- SQL查询请求模型,灵活度高,继承自QueryRequest
- WhereItem -- 查询条件,需要同时传入条件表达式和参数值,在TabQuery中有若干个WhereItem
com.integ.eao包是关于实体处理的类,主要类包括:
- IEntityAccessObject -- 实体访问对象接口,提供实体对象的增删改查等方法。根据实际情况可以有多种不同的实现,比如操作关系数据库的,操作NoSQL数据库的,操作Hadoop的,都可以分别实现
- EntityAccessAdapter -- EAO适配器接口,提供createNewId()方法和getDao()方法,建立EntityAccessObject 时需要用到。
- EntityAccessObject -- IEntityAccessObject的基本实现类,实现了对关系型数据库的操作,构造时需要传入EntityAccessAdapter 对象
- Entity -- 实体类的基础类。有id,name,createTime三个属性。Entity类是所有实体类的父类。实体类定义时要使用EntityAnno注解
- EntityAnno -- 实体类的注解。指定表名,表核心名,实体类定义时必须用到。
- EntityModel -- 实体模型信息。采集了关于实体模型的各种信息,比如对应的表,数据库字段,属性信息等。由EntityModelBuilder建造生成。包含一个到多个FieldInfo。
- FieldInfo -- 属性信息类。存放属性元数据,比如属性名称,对应的字段名,属性反射对象等。
- EntityModelBuilder -- EntityModel 的构造类。采集实体类信息和对应的库表信息,构造生成实体类的EntityModel。构造时需要传入DataAccessObject。
- IdGenerator -- 提供多种产生新ID的方法 。
一些重要的类的方法
DAOBuilder
- DataAccessObject createDAO(JdbcConfig config)
- DataAccessObject createDAO(File jdbcConfigFile)
DataAccessObject
- List query(QueryRequest request, RowMapper rowMapper)
- List query(String sql, Object[] values, RowMapper rowMapper)
- int queryCount(QueryRequest request)
- int queryCount(String tableName, String whereStmt, Object... values)
- void insert(String tableName, Map<String, Object> cols)
- void update(String tableName, Map<String,Object> updateValues, WhereItem whereItem)
- void deleteById(String tableName, String keyColumn, Object id)
- int update(String sql, Object... values)
SqlBuilder
- String makeQuerySql()
- String makeQueryCountSql()
IEntityAccessObject
- T getById(Object id)
- T insert(T entity)
- void deleteById(Object id)
- void update(T entity, String[] fieldNames)
- void update(T entity, String fieldNames)
- List<T> query(QueryRequest queryReq)
- int queryCount(String whereStmt, Object... values)
- PageData<T> pageQuery(QueryRequest req)
- (EntityAccessObject的主要方法与IEntityAccessObject一致)
EntityAccessAdapter
- DataAccessObject getDao()
- Object createNewId()
测试
先建表
CREATE TABLE `tb_student` (
`student_id` varchar(20) NOT NULL,
`student_name` varchar(100) NOT NULL,
`school_class_id` smallint(4) NOT NULL,
`sex` tinyint(1) default NULL,
`birthday` date default NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`student_id`)
) DEFAULT CHARSET=utf8;
编写JUnit测试代码,从测试代码中也可以看到EAO的使用方法。
package com.integ.test;
import java.util.List;
import org.junit.*;
import com.integ.dao.*;
import com.integ.eao.*;
import com.integ.log.Log4jHelper;
public class EAOTest {
static EntityAccessObject<Student> eao ;
@BeforeClass
public static void beforeAll() {
StudentAdapter adapter = new StudentAdapter();
eao = new EntityAccessObject<Student>(adapter);
Log4jHelper.initLogger();
}
@Before
public void before() {
eao.getDAO().update("truncate table tb_student");
}
@Test
public void testCUD() {
final String id = "s1";
Student s1 = new Student();
s1.setId(id);
s1.setName("小明");
eao.insert(s1);
Student s2 = eao.getById(id);
Assert.assertNotNull(s2);
s2.setName("小华");
eao.update(s2, "name");
Student s3 = eao.getById(id);
Assert.assertNotEquals(s1.getName(), s3.getName());
Assert.assertEquals(s2.getName(), s3.getName());
eao.deleteById(id);
Student s4 = eao.getById(id);
Assert.assertNull(s4);
}
@Test
public void testQuery() {
Student s1 = new Student();
s1.setId("s1");
s1.setName("小明");
s1.setSchoolClassId(1);
eao.insert(s1);
Student s2 = new Student();
s2.setId("s2");
s2.setName("小华");
s2.setSchoolClassId(2);
eao.insert(s2);
TabQuery req = new TabQuery();
req.addWhereItem("school_class_id=?", 2);
List<Student> list = eao.query(req);
Assert.assertEquals(1, list.size());
SqlQuery sqlReq = new SqlQuery("select * from tb_student where student_id=?", "s1");
list = eao.query(sqlReq);
Assert.assertEquals(1, list.size());
int count = eao.queryCount("school_class_id=?", 2);
Assert.assertEquals(1, count);
}
@Test
public void testPageQuery() {
for (int i=0; i<100; i++) {
Student s1 = new Student();
s1.setName(IdGenerator.createRandomStr(12, false));
s1.setSchoolClassId(3);
eao.insert(s1);
}
TabQuery tq = new TabQuery();
tq.addWhereItem("school_class_id=?", 3);
tq.setStart(0);
tq.setLimit(10);
PageData<Student> page = eao.pageQuery(tq);
Assert.assertEquals(100, page.getTotalCount());
Assert.assertEquals(10, page.getList().size());
}
}
package com.integ.test;
import com.integ.dao.DataAccessObject;
import com.integ.eao.EntityAccessAdapter;
import com.integ.eao.IdGenerator;
public class StudentAdapter implements EntityAccessAdapter<Student> {
@Override
public Object createNewId() {
return IdGenerator.createRandomStr(12, false);
}
@Override
public DataAccessObject getDao() {
return DaoUtil.getDao();
}
}
package com.integ.test;
import java.util.Date;
import com.integ.eao.Entity;
import com.integ.eao.EntityAnno;
@EntityAnno(table="tb_student")
public class Student extends Entity {
private Integer sex;
private Date birthday;
private int schoolClassId;
public Integer getSex() { return sex; }
public void setSex(Integer sex) { this.sex = sex; }
public Date getBirthday() { return birthday; }
public void setBirthday(Date birthday) { this.birthday = birthday; }
public int getSchoolClassId() { return schoolClassId; }
public void setSchoolClassId(int schoolClassId) { this.schoolClassId = schoolClassId; }
}
package com.integ.test;
import com.integ.dao.DAOBuilder;
import com.integ.dao.DataAccessObject;
import com.integ.dao.JdbcConfig;
public class DaoUtil {
private static DataAccessObject dao;
public static DataAccessObject getDao() {
if (dao==null) {
JdbcConfig config = new JdbcConfig();
config.setDriverClassName("com.mysql.jdbc.Driver");
config.setUrl("jdbc:mysql://localhost:3306/study?useUnicode=true&autoReconnect=true&failOverReadOnly=false&characterEncoding=utf-8");
config.setUserName("root");
config.setPassword("123456");
dao = new DAOBuilder().createDAO(config);
}
return dao;
}
}
测试结果:增删改、普通查询、分页查询,三个测试案例都通过了。
ORM基础功能开发完成。
如果有兴趣可以 在这里下载源代码
之后要面对的挑战,是属性映射、数据缓存和查询缓存、事务管理。
属性映射相当于关联查询,比如我们查询学生信息的时候,会想要同时看到该学生所属班级的名称,但是在学生表里面只有班级的ID,所以要通过班级ID找到班级对象,把班级名称属性复制过来,这就是属性映射。
数据缓存是把实体对象在内存中存储起来,查询缓存是把查询结果缓存起来,两者都是为了减少对数据库的访问,减少访问就可以承受更大的负载,以及获得更高的响应速度。
事务管理,是指一个业务处理有一系列的数据库操作,一系列操作是不可分割的,要么全部成功,要么全部失败。本框架要实现这种事务的一致性。
附录:EntityAccessObject类代码
package com.integ.eao;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.jdbc.core.RowMapper;
import com.integ.dao.DataAccessObject;
import com.integ.dao.QueryRequest;
import com.integ.dao.TabQuery;
import com.integ.dao.WhereItem;
import com.integ.utils.Convertor;
import com.integ.utils.StringUtils;
public class EntityAccessObject<T extends Entity> implements IEntityAccessObject<T> {
private DataAccessObject dao;
private EntityModel em;
private Class<T> entityClass;
private final RowMapper rowMapper;
private EntityAccessAdapter<T> adapter;
public EntityAccessObject(EntityAccessAdapter<T> adapter) {
this.adapter = adapter;
this.dao = adapter.getDao();
this.entityClass = getEntityClass();
//System.out.println("entityClass="+entityClass);
EntityModelBuilder emBuilder = new EntityModelBuilder(entityClass, dao);
this.em = emBuilder.buildModel();
rowMapper = new RowMapper(){
@Override
public Object mapRow(ResultSet rset, int row) throws SQLException {
T entity = null;
try {
entity = entityClass.newInstance();
fillFieldValues(entity, rset);
}
catch(Exception e) {
e.printStackTrace();
}
return entity;
}
};
}
@SuppressWarnings("unchecked")
private Class<T> getEntityClass() {
Type t = adapter.getClass().getGenericInterfaces()[0];
Type[] ts = ((ParameterizedType) t).getActualTypeArguments();
return (Class<T>)ts[0];
}
public DataAccessObject getDAO() {
return dao;
}
private void fillFieldValues(T entity, ResultSet rset) throws Exception {
FieldInfo field;
Object value;
for (String fieldName : em.getAllFields()) {
field = em.getFieldInfo(fieldName);
if (field.columnExists()) {
value = getColumnValue(rset, field.getColumnName(), field.getField().getType());
//System.out.println("before set field value: field="+field.getName()+", value="+value);
setFieldValue(entity, field, value);
}
}
}
@SuppressWarnings("rawtypes")
public static Object getColumnValue(ResultSet rset, String columnName, Class dataType)
throws SQLException {
String className = dataType.getSimpleName();
Object value = null;
if (className.equals("String")) {
value = rset.getString(columnName);
}
else if (className.equals("int")) {
value = rset.getInt(columnName);
}
else if (className.equals("Integer")||className.equals("Long")) {
value = rset.getObject(columnName);
value = Convertor.translate(value, dataType);
}
else if (className.equalsIgnoreCase("long")) {
value = rset.getLong(columnName);
}
else if (className.equals("Date")) {
value = rset.getTimestamp(columnName);
}
return value;
}
@SuppressWarnings("unchecked")
@Override
public T getById(Object id) {
String sql = "select * from "+em.tableName()+" where "+em.keyColumn()+"=?";
List<T> list = dao.query(sql, new Object[]{id}, rowMapper);
return getFirst(list);
}
protected T getFirst(List<T> list) {
return list==null||list.size()==0?null:list.get(0);
}
@Override
public T insert(T entity) {
String keyValue = entity.getId();
if (keyValue==null) {
keyValue = this.createNewIdNoRepeat();
entity.setId(keyValue);
}
if (em.columnExists(Columns.CREATE_TIME)) {
if (entity.getCreateTime()==null) {
entity.setCreateTime(new Date());
}
}
String[] fieldNames = em.allFields;
Map<String, Object> colValues = new HashMap<>();
Object value;
FieldInfo field;
for (String fieldName: fieldNames) {
field = em.getFieldInfo(fieldName);
if (field.columnExists()) {
value = field.getValue(entity);
if (value!=null) {
value = Convertor.toString(value);
colValues.put(field.getColumnName(), value);
}
}
}
dao.insert(em.tableName(), colValues);
return entity;
}
protected String createNewIdNoRepeat() {
String newId;
int testCount = 0, count;
do {
newId = adapter.createNewId().toString();
count = queryCount(em.keyColumn()+"=?", newId);
testCount++;
} while (count>0 && testCount<10);
if (testCount>=10) {
throw new Error("产生主键值程序有错误,已连续产生了多个重复主键!");
}
return newId;
}
@Override
public int queryCount(String whereStmt, Object... values) {
return dao.queryCount(em.tableName(), whereStmt, values);
}
@Override
public void deleteById(Object id) {
String sql = "delete from "+em.tableName()+" where "+em.keyColumn()+"=?";
dao.update(sql, id);
}
@Override
public void update(T entity, String[] fieldNames) {
String fieldName, colName;
Object value;
FieldInfo field;
Map<String, Object> updateFields = new HashMap<String, Object>();
for (int i=0; i<fieldNames.length; i++) {
fieldName = fieldNames[i];
field = em.getFieldInfo(fieldName);
if (field==null) {
throw new Error(entityClass.getSimpleName()+"."+fieldName+" 属性不存在");
}
colName = field.getColumnName();
if(!field.columnExists()) {
throw new Error(fieldName+"->"+colName+" 字段不存在!");
}
value = field.getValue(entity);
updateFields.put(colName, value);
}
if (updateFields.size()==0) {
return;
}
WhereItem where = new WhereItem(em.keyColumn+"=?", entity.getId());
dao.update(em.tableName(), updateFields, where);
}
@Override
public void update(T entity, String fieldNames) {
String[] fields = StringUtils.split(fieldNames, ",");
update(entity, fields);
}
@SuppressWarnings("unchecked")
@Override
public List<T> query(QueryRequest req) {
if (req instanceof TabQuery) {
TabQuery tq = (TabQuery)req;
tq.setTableName(em.tableName);
tq.setKeyColumn(em.keyColumn);
}
List<T> list = dao.query(req, rowMapper);
return list;
}
@SuppressWarnings("unchecked")
@Override
public PageData<T> pageQuery(QueryRequest req) {
if (req instanceof TabQuery) {
TabQuery tq = (TabQuery)req;
tq.setTableName(em.tableName);
tq.setKeyColumn(em.keyColumn);
}
int count = dao.queryCount(req);
List<T> list = dao.query(req, rowMapper);
return new PageData<T>(list, count);
}
private void setFieldValue(T entity, FieldInfo field, Object value) throws Exception {
if (field.getSetter()!=null) {
Object val = Convertor.translate(value, field.getField().getType());
field.getSetter().invoke(entity, val);
}
}
}