JAVA SE 进阶篇 C8 简单ORM框架的实现

P1 关于ORM

1 Object Relation Mapping 对象关系数据库映射

ORM其主旨思想是:
1,表和类,记录和对象,这两者之间存在对应关系
2,一个表对应一个类,表中的列和类的成员有一一对应的关系
3,一条记录对应一个对象

如果能根据一个类,知道这个类与一个表一一对应,同时,类成员和表字段的映射关系也可以得到的话,那么可以根据这个类,自动创建并执行一个SQL语句,最终直接生成对应类的对象,也就是将由类得到SQL语句,由SQL语句执行得到对应对象的过程完全自动化

P2 实现一个简单映射

1 ClassTable和PropertyColumn类

由于ORM工具也是要面向未来的,它要处理的是以后传入的数据,所以需要使用到反射机制,我们先做一个简单的实验,由一个类名来取得它的成员和默认映射(与成员名相同)的打印结果

(1) PropertyColumn类

由于一个类对应一张表,类中的成员一般都大于1,所以对应的列也应大于1,则相应的成员-列映射关系也大于1,做一个PropertyColumn类来作为每个映射的模板,即一个成员,一个列
在这里插入图片描述

(2) ClassTable类

每个类对应一个相应的表,其中应该存储着所有的成员-列映射关系,所以需要ClassTable类能根据未来类的字符串来获取它的所有成员,给出默认的成员-列映射,

package com.coisini.orm.core;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * 表和类的映射
 * @author coisini1999
 */
public class ClassTable {
	
	private Class<?> klass;
	private String table;
	
	//一个表中有多个列,一个类中有多个成员
	//映射关系大都是大于一对的,所以用List存
	//若干对类的成员和表的列的映射
	private List<PropertyColumn> fields;
	
	//fields中每个映射的下标,用于控制循环
	private int fieldIndex;
	
	
	
	//无参构造初始化一个ArrayList类的
	//fields用来存储成员,列的映射
	public ClassTable() {
		this.fields = new ArrayList<PropertyColumn>();
		this.fieldIndex = 0;
	}
	
	public Class<?> getKlass() {
		return klass;
	}
	
	/**
	 * 由反射机制获取需要处理的类的所有成员存储到Fields[]中
	 * 对每个成员设置成员,列映射对象
	 * 存储到fields中
	 */
	public void setklass(String className) throws Exception {
		
		this.klass = Class.forName(className);
		
		Field[] fieldList = this.klass.getDeclaredFields();
		for(Field field : fieldList) {
			PropertyColumn pc = new PropertyColumn();
			pc.setProperty(field);
			//默认列和成员名字相同
			pc.setColumn(field.getName());
			
			this.fields.add(pc);
		}			
	}
	
	//hasNext()和next()用于循环中取出fields中每个成员
	//配合getColumn可以取出fields中所有映射
	
	/**
	 * 根据fieldIndex判断fields是否还有下一个
	 */
	public boolean hasNext() {
		return this.fieldIndex < this.fields.size();
	}
	
	/**
	 * 用于取出fields中的成员
	 */
	public Field next() {
		Field field = this.fields.get(this.fieldIndex).getProperty();
		this.fieldIndex++;
		
		return field;
	}
	
	/**
	 * 从成员,列的映射列表中
	 * 根据成员获取列名
	 */
	public String getColumn(Field property) {
		
		for(PropertyColumn pc : this.fields) {
			if(pc.getProperty().equals(property)) {
				return pc.getColumn();
			}
		}
		
		return null;
	}
	
	
	public String getTable() {
		return table;
	}

	public void setTable(String table) {
		this.table = table;
	}	

}

2 简单地输出映射关系

有了上面的两个类后,做一个测试类并执行:
在这里插入图片描述
上述的简单例子是StudentModel类,经过ClassTable中的setKlass先获取其中的所有成员,并根据这些成员获取其默认列名,并由hasNext()和next()在遍历中输出映射关系

虽然这个简单例子没有什么用,但为接下来ROM工具的实现提供了基础

现在由一个XML文件,指明类对应的表,成员和列的映射,希望以后提供一个类就可以在Dao层自动生成SQL语句并执行

P3 ORM框架的实现

为了获得一个ORM工具,就必须得先获得表和类,列和成员的映射关系,以上一节的例子中,sys_student_info表和StudentModel类示例:

sys_student_info表:
在这里插入图片描述
StudentModel类:
在这里插入图片描述

1 由成员是否有get方法得到合理的成员-列映射

注意上面的简单输出示例,输出了sdf,但sdf不是我们所需要的成员,有效的成员都有一套set和get方法,这里通过是否有get方法来找到合理的成员-列映射

接下来完善ClassTable类,通过成员是否有get方法来取得有效的成员-列映射:

	/**
	 * 由反射机制获取需要处理的类的所有成员存储到Fields[]中
	 * 对每个成员设置成员,列映射对象
	 * 存储到fields中
	 * 检测每个成员是否有get方法(该成员是否合理)
	 */
	public void setKlass(String className)  {
		
			try {
				this.klass = Class.forName(className);
				
				Field[] fieldList = this.klass.getDeclaredFields();
				
				for(Field field : fieldList) {
					//获取每个成员的get方法名字,boolena类型的get方法是isXXX
					String fieldName = field.getName();
					String getterName = "get";
					if(field.getType().equals(boolean.class)) {
						getterName = "is";
					}
					
					getterName += fieldName.substring(0,1).toUpperCase() 
							+ fieldName.substring(1);
					
					//检测成员是否有get方法,有则生成相应映射,无则跳过处理下一个成员直到结束
					//这样处理异常的手段用来避免有一个成员没找到get方法如sdf抛异常终止程序
						try {
							this.klass.getDeclaredMethod(getterName, new Class<?>[] {});
							PropertyColumn pc = new PropertyColumn();
							pc.setProperty(field);
							pc.setColumn(fieldName);
							this.fields.add(pc);
						} catch (NoSuchMethodException e) {
						} catch (SecurityException e) {
						}
								
				}
			
				
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} 
				
	}

再运行Test类,观察结果:
在这里插入图片描述
虽然避免了不合理的成员被加到映射集中,但是上述的程序还是不完整的,需要由一个给定表和类对应关系和成员,列对应关系的XML文件来获取正确的映射

XML文件:
在这里插入图片描述

2 根据XML文件,获取正确的映射类

为了从XML文件中获取正确的映射,ClassTable需要新增几个方法

	/**
	 * 根据XML文件中的主键说明
	 * 设置主键
	 */
	public void setKey(String keyName) {
		for(PropertyColumn pc : this.fields) {
			if(pc.getProperty().getName().equals(keyName)) {
				this.key = pc;
				return;
			}
		}	
	}
	
	public PropertyColumn getKey() {
		return this.key;
	}
	
	
	/**
	 * 根据XML文件中映射的成员名和列名
	 * 设置成员对应的列名
	 */
	public void setColumnName(String propertyName, String columnName) {
		for(PropertyColumn pc : this.fields) {
			if(pc.getProperty().getName().equals(propertyName)) {
				pc.setColumn(columnName);
				return;
			}
		}	
	}

还需要一个类来解析XML文件,它需要XML文件的路径,根据所给的类名来返回一个该类的ClassTable类,即正确的映射关系类

package com.coisini.orm.core;

import java.util.HashMap;
import java.util.Map;

import javax.management.modelmbean.XMLParseException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.my.util.XMLParser;

public class ClassTableFactory {
	
	private static final Map<String, ClassTable> classTablePool;
	
	static {
		classTablePool = new HashMap<>();
	}
	
	public ClassTableFactory() {
	}
	
	
	/**
	 * 扫描XML文件
	 */
	public static void scanClassTableMapping(String xmlPath) throws Throwable {
		
		Document doc = XMLParser.newDocument(xmlPath);
		
		//解析第一层,获得类名和表名的映射
		new XMLParser() {
			@Override
			public void dealElement(Element element, int index) throws Throwable {
				String className = element.getAttribute("name");
				String tableName = element.getAttribute("table");
				
				ClassTable ct = new ClassTable();
				ct.setKlass(className);
				ct.setTable(tableName);
				
				//解析第二层property,得到propertyName和columnName
				//给映射的列取名
				new XMLParser() {
					@Override
					public void dealElement(Element element, int index) throws Throwable {
						
						String propertyName = element.getAttribute("name");
						String columnName  =element.getAttribute("column");
						ct.setColumnName(propertyName, columnName);
						
					}
				}.parseTag(element, "porperty");
				
				//解析第二层key,得到keyName
				//设置为主键名
				new XMLParser() {
					@Override
					public void dealElement(Element element, int index) throws Throwable {
						String keyName = element.getAttribute("name");
						ct.setKey(keyName);
					}
				}.parseTag(element, "key");
				
				//解析完后,一个类名对应一个类成员-表列的映射关系
				classTablePool.put(className, ct);
				
			}
		}.parseTag(doc, "class");
		
	}
	
	
	//两种重载方法获得给定类的ClassTable
	public static ClassTable getClassTable(String className) {
		return classTablePool.get(className);
	}
	
	public static ClassTable getClassTable(Class<?> klass) {
		return classTablePool.get(klass.getName());
	}	

}

测试:
在这里插入图片描述

现在就可以得到正确的映射,但是这只是从XML文件中进行的映射,最终目的是由类到数据库表取得类到表的成员-列映射,从而自动生成SQL语句

3 做一个获取数据库连接的工具

重新设计一个数据库连接类,由properties文件获取连接信息:

package com.coisini.orm.core;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import com.my.util.PropertiesNotFound;
import com.my.util.PropertiesParser;

public class MyDataBase {
	
	private static Connection connection;
	
	private static String driver;
	private static String url;
	private static String user;
	private static String password;
	
	
	public static Connection getConnection(String configFilePath) 
			throws IOException, PropertiesNotFound, ClassNotFoundException, SQLException {
		
		if(connection == null) {
			PropertiesParser.loadProperties(configFilePath);
			driver = PropertiesParser.getValue("driver");
			url = PropertiesParser.getValue("url");
			user = PropertiesParser.getValue("user");
			password = PropertiesParser.getValue("password");
			
		}
		
		
		
		return getConnection();
	}
	
	
	public static Connection getConnection() 
			throws ClassNotFoundException, SQLException {
		
		if(connection == null) {
			Class.forName(driver);
			connection = DriverManager.getConnection(url, user, password);
		}
		
		return connection;
		
	}
	
}

数据库连接配置信息properties文件:
在这里插入图片描述

4 ORM中设置基本的SQL语句

(1) 基本SELLECT语句

	/**
	 * 从映射类中取出类对应的表名
	 * 和成员对应的列名,通过循环将每个列名加入到sql语句中
	 * 获得最基本的查询表中所有列的sql语句
	 */
	private StringBuffer baseSelectSQL(ClassTable ct) {
		
		//获取表名
		String table = ct.getTable();
		
		//构造SQL语句
		StringBuffer sql = new StringBuffer();
		sql.append("SELECT ");
		
		//第一个列名前不加 ','后续的除最后一个列名外
		//都需要加上 ','分隔
		boolean first = true;
		
		//通过hasNext和next方法取得表中的每个列名加入到sql语句中
		while(ct.hasNext()) {
			PropertyColumn pc = ct.next();
			sql.append(first ? "" : ", ")
				.append(table).append(".").append(pc.getColumn());
			first = false;
		}
		sql.append(" FROM ").append(table);
		
		
		return sql;
	}

通过这个方法就可以将未来XML文件中类和表,成员和列的映射关系,做一个最基本的SELECT语句,用于查询该表中的所有列

(2) 基本INSERT INTO语句

	/**
	 * 从映射类中取出类对应的表名
	 * 和成员对应的列名,通过循环将INSERT INTO后的每个列名先加入SQL语句
	 * 对于VALUES后的具体值,先加入?
	 * 具体方法中使用用户提供的对象的各成员的值用setObject替换?
	 */
	private StringBuffer baseInsertSQL(ClassTable ct) {
		
		StringBuffer sql = new StringBuffer();
		
		String table = ct.getTable();
		sql.append("INSERT INTO ").append(table).append(" (");
		
		//第一个列名前不加 ','后续的除最后一个列名外
		//都需要加上 ','分隔
		int columnCount = 0;
		
		//通过hasNext和next方法取得表中的每个列名加入到sql语句中
		while(ct.hasNext()) {
			PropertyColumn pc = ct.next();
			sql.append(columnCount == 0 ? "" : ", ");
			sql.append(table).append('.').append(pc.getColumn());
			columnCount++;
		}
		
		sql.append(") VALUES(");
		
		//这里构造SQL语句时由于不知道要插入的值
		//所以先全部用?代替,最后在具体方法中用setObject方法用具体的值替换?
		for(int i = 0; i < columnCount; i++) {
			sql.append(i == 0 ? "" : ", ").append('?');
		}
		sql.append(')');
		
		return sql;
	}

5 将SQL语句返回的值赋值给要返回的泛型对象

	/**
	 * 取得查询SQL语句的结果集中的值转换成Object类的成员
	 * 通过反射机制取得该类中的每个成员的set方法
	 * 通过执行set方法,将Object类的成员赋值给返回结果对象中的成员
	 */
	private Object setOneObject(ClassTable ct, ResultSet rs) 
			throws InstantiationException, IllegalAccessException, 
				SQLException, NoSuchMethodException, SecurityException, 
					IllegalArgumentException, InvocationTargetException {
		
		
		//利用反射机制生成一个需要返回类的该类对象
		Class<?> klass = ct.getClass();
		Object result = klass.newInstance();
		
		while(ct.hasNext()) {
			
			//取得每个列所对应的Object类成员
			PropertyColumn pc = ct.next();
			String column = pc.getColumn();
			Object property = rs.getObject(column);
			
			//通过反射机制取得每个成员,再取得它们的set方法
			//执行set方法将property赋值给返回对象的成员
			Field field = pc.getProperty();
			
			String fieldName = field.getName();
			String methodName = "set" + 
					fieldName.substring(0, 1).toUpperCase()+fieldName.substring(1);
			
			Method method = 
					klass.getDeclaredMethod(methodName, new Class<?>[] {field.getType()});
			
			method.invoke(result, new Object[] {property});
			
			return result;
			
		}

6 利用ORM框架获取数据库中的一条记录

为了让框架更泛用,不单单处理StudentModel类,采用泛型来处理,获取一条记录,根据baseSelectSQL语句先获取SQL语句的基本框架,再根据用户传入的Object类的主键id,用setObject替换?,通过反射机制取得类中每个成员的set方法,将查询的结果作为一个结果集,通过setOneObject方法,将rs中的值取出作为set方法的参数,返回一个泛型类的对象

/**
	 * 根据主键值查询一条记录
	 */
	public <T> T get(Class<?> klass, Object id) 
			throws ClassNotFoundException, SQLException, 
				InstantiationException, IllegalAccessException, 
					NoSuchMethodException, SecurityException, 
						IllegalArgumentException, InvocationTargetException {
		
		Object result = null;
		
		//获取表名和主键名
		ClassTable ct = ClassTableFactory.getClassTable(klass);
		String table = ct.getTable();
		String keyField = table + "." + ct.getKey().getColumn();
		
		//获取基本SQL语句并添加查询主键的where子句,条件先用?代替
		StringBuffer sql = baseSelectSQL(ct);
		sql.append(" WHERE ").append(keyField).append("=?");
				
		
		//执行SQL语句
		PreparedStatement state = getConnection().prepareStatement(sql.toString());
		//将sql语句中的第一个?替换成输入的主键id
		state.setObject(1, id);
		
		ResultSet rs = state.executeQuery();
		
		if(rs.next()) {
			result = setOneObject(ct, rs);
		}

		return (T)result;
	}

类似的,获得数据库中的所有记录:

/**
	 * 通过传递类,获取该类所对应表的所有记录
	 */
	public <T> List<T> getList(Class<?> klass) 
			throws ClassNotFoundException, SQLException, 
				InstantiationException, IllegalAccessException, 
					NoSuchMethodException, SecurityException, 
						IllegalArgumentException, InvocationTargetException {
		
		//主备好存储<T>的ArrayList和映射对象ct
		List<T> result = new ArrayList<T>();
		ClassTable ct = ClassTableFactory.getClassTable(klass);
		
		//准备并执行SQL语句
		StringBuffer sql = baseSelectSQL(ct);
		PreparedStatement state = getConnection().prepareStatement(sql.toString());
		ResultSet rs = state.executeQuery();
		
		//根据结果集中的每一条记录通过setOneObject方法产生一个Object类的对象
		//加到要返回的列表result中,最后再强转result的类型为T
		while(rs.next()) {
			Object object = setOneObject(ct, rs);
			result.add((T)object);
		}
		
		return result;
			
	}

7 利用ORM框架向数据库插入一条记录

/**
	 * 根据所给的对象,向数据库中插入一条记录
	 */
	public <T> int save(T object) throws ClassNotFoundException, SQLException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		
		//获取object的类型,根据该类型得到映射类
		Class<?> klass = object.getClass();
		ClassTable ct = ClassTableFactory.getClassTable(klass);
		
		//准备SQL语句
		StringBuffer sql = baseInsertSQL(ct);
		PreparedStatement state = getConnection().prepareStatement(sql.toString());
		
		int index = 1;
		
		while(ct.hasNext()) {
			
			//获取映射类中类的每个成员的get方法
			PropertyColumn pc = ct.next();
			Field field = pc.getProperty();
			String fieldName = field.getName();
			
			String methodName = 
					(field.getType().equals(boolean.class) ? "is" : "get")
						+ fieldName.substring(0, 1).toUpperCase()
						+ fieldName.substring(1);
			Method method = klass.getDeclaredMethod(methodName, new Class<?>[] {});
			
			//执行get方法,取得作为参数的object对象的每个成员的值
			Object propertyValue = method.invoke(object);
			
			//用propertyValue替换VALUES后的若干个?,完善INSERT INTO语句
			state.setObject(index++, propertyValue);
			
		}
		
		//执行并返回SQL语句的结果
		return state.executeUpdate();	
		
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值