在Java应用开发中,凡是有数据库操作的项目都要面对ORM(数据对象映射)的问题。一个优秀的ORM框架可以省下大量的重复代码,屏蔽数据库操作的细节,让人们把主要注意力都放在业务逻辑上,从而提高开发工作的效率和质量。
常见的Java应用ORM框架有Hibernate,MyBatis等,Spring全家桶里面也有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包是和数据库处理相关的类,主要类包括:
com.integ.eao包是关于实体处理的类,主要类包括:
一些重要的类的方法
DAOBuilder
DataAccessObject
SqlBuilder
IEntityAccessObject
EntityAccessAdapter
先建表
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 eao ;
@BeforeClass
public static void beforeAll() {
StudentAdapter adapter = new StudentAdapter();
eao = new EntityAccessObject(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 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 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 {
@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 implements IEntityAccessObject {
private DataAccessObject dao;
private EntityModel em;
private Class entityClass;
private final RowMapper rowMapper;
private EntityAccessAdapter adapter;
public EntityAccessObject(EntityAccessAdapter 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 getEntityClass() {
Type t = adapter.getClass().getGenericInterfaces()[0];
Type[] ts = ((ParameterizedType) t).getActualTypeArguments();
return (Class)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 list = dao.query(sql, new Object[]{id}, rowMapper);
return getFirst(list);
}
protected T getFirst(List 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 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 updateFields = new HashMap();
for (int i=0; i"+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 query(QueryRequest req) {
if (req instanceof TabQuery) {
TabQuery tq = (TabQuery)req;
tq.setTableName(em.tableName);
tq.setKeyColumn(em.keyColumn);
}
List list = dao.query(req, rowMapper);
return list;
}
@SuppressWarnings("unchecked")
@Override
public PageData 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 list = dao.query(req, rowMapper);
return new PageData(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);
}
}
}