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();
}