天天看点

自己动手编写Java ORM框架(一)基本功能前言实现ORM基本功能

前言

在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。

类结构图

先看看类图:

自己动手编写Java ORM框架(一)基本功能前言实现ORM基本功能
自己动手编写Java ORM框架(一)基本功能前言实现ORM基本功能

可以在这里下载本篇文章的源代码: 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);
		}
	}
	
}