Mybatis源代码分析之metadata包
数据库中我们最常用到的元素就是database、table、column。Mybatis作为强大的ORM框架,当中也包含了获取处理这些元素的代码,本篇将介绍相关的内容。
一、Catalog & Schema
在介绍具体的代码实现之前,我们需要先了解两个后面会用到的两个名词:Catalog & Schema。
按照SQL标准的解释,在SQL环境下Catalog和Schema都属于抽象概念,可以把它们理解为一个容器或者数据库对象命名空间中的一个层次,主要用来解决命名冲突问题。从概念上说,一个数据库系统包含多个Catalog,每个Catalog又包含多个Schema,而每个Schema又包含多个数据库对象(表、视图、字段等),反过来讲一个数据库对象必然属于一个Schema,而该Schema又必然属于一个Catalog,这样我们就可以得到该数据库对象的完全限定名称从而解决命名冲突的问题了;例如数据库对象表的完全限定名称就可以表示为:Catalog名称.Schema名称.表名称。这里还有一点需要注意的是,SQL标准并不要求每个数据库对象的完全限定名称是唯一的,就像域名一样,如果喜欢的话,每个IP地址都可以拥有多个域名。
从实现的角度来看,各种数据库系统对Catalog和Schema的支持和实现方式千差万别,针对具体问题需要参考具体的产品说明书,比较简单而常用的实现方式是使用数据库名作为Catalog名,使用用户名作为Schema名,具体可参见下表:
最后一点需要注意的是Schema这个单词,它在SQL环境下的含义与其在数据建模领域中的含义是完全不同的。在SQL环境下,Schema是一组相关的数据库对象的集合,Schema的名字为该组对象定义了一个命名空间,而在数据建模领域,Schema(模式)表示的是用形式语言描述的数据库的结构;简单来说,可以这样理解,数据建模所讲的Schema<也就是元数据>保存在SQL环境下相应Catalog中一个Schema<名叫DEFINITION_SCHEMA>下的表中,同时可以通过查询该Catalog中的另一个Schema<名叫INFORMATION_SCHEMA>下的视图而获取,具体细节不再赘述。
注明:该小节的内容来自于博客http://blog.sina.com.cn/s/blog_515015800100evtc.html
二、Database、Table、Column
按照包容关系,从大到小的顺序为database、table、column。因而在介绍的时候我们将从最小的开始,也就是从Column开始,而后是Table,最后是database的顺序进行。
1.Column
对于数据库中的列而言,主要有这么几个属性:名称、类型(有些有精度或者长度)、默认值、是否可为空、注释等。熟悉mysql的读者可以看看information_schema库中的COLUMNS表,这个表中的每一条记录描述了一列的各种属性。在mybatis的Column类只记录常用的两个属性:列名和类型
//列名 private String name; /** * 类型,具体的数字代表的类型可以参考java.sql.Types中一系列的静态常量 * 如 public final static int BIT = -7; * public final static int INTEGER = 4; * public final static int DOUBLE = 8; */ private int type;
这个类中的函数都是一些常用的基本函数,没有什么特殊的地方,也相当的简单明了,有兴趣的读者可以直接看源代码。个人觉得equals函数的实现挺有意思,在此做下说明,与大家分享:
/** * 有必要提下 “==”和equals的区别和联系 * “==” 比较的是两个变量在内存中的是否指向同一个地方,即内存地址是否相同 * “equals” 比较的是两个对象的内容是不是相同 * 因而 “==”的true的两个变量“equals”一定为true,但反之则不然.有兴趣的同学可以执行下下面几行代码: * String a = new String("22"); String b = new String("22"); String csString = a; System.out.println(a==b?"====":"!==="); System.out.println(a.equals(b)?"equals":"!equals"); System.out.println(a==csString?"====":"!==="); * */ public boolean equals(Object o) { // 比较这两个变量的内存地址,如果指向同一块内存,则必然相等 if (this == o) return true; // 如果两个对象的类型都不一样,内容肯定也不一样,要不然也无需定义两个不同的类了 if (o == null || getClass() != o.getClass()) return false; final Column column = (Column) o; //比较类型,如果类型不同则必然是两个不同的列 if (type != column.type) return false; //请看下面这条语句,写的相当巧妙,一行语句针对两种请看做了处理,比if else要好 //如果这个变量的name不为空,则判断这两个变量的类型名称是不是相同(用equals,而不是==);如果为空,则判断另一个变量的name是不是为空 if (name != null ? !name.equals(column.name) : column.name != null) return false; //当然,也可以先比较名称然后再比较名称, return true; }
注意,在重写equals方法时,要注意满足离散数学上的特性
1 自反性 :对任意引用值X,x.equals(x)的返回值一定为true.
2 对称性: 对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
3 传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true
4 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
5 非空性:任何非空的引用值X,x.equals(null)的返回值一定为false
2.Table
接下来我们来看Table这个类,数据库中的表通常有这么几个属性:表名、一些列、主键、索引、表的注释、表的引擎类型、表的编码格式等。在mybatis中Table类的属性有如下几个:
private final String name;//表名 private String catalog;//表的catalog,具体哪些数据库支持该属性请见第一节 private String schema;//表的schema,对于mysql而言,就是库名 //包含的列,key的列的名称,value是列的具体信息 private final Map<String, Column> columns = new HashMap<String, Column>(); //主键,看这个定义不支持联合主键 private Column primaryKey;
这个类中的函数没有什么特殊之处,都是常见见的,在此不再进行说明。
3.Database
接下来我们来看Database这个类,在mybatis中Database类的属性有如下几个:
private final String catalog;//库的catalog,具体哪些数据库支持该属性请见第一节 private final String schema;//可以看做库名 //该数据库包含的表 private final Map<String, Table> tables = new HashMap<String, Table>();
这个类中的函数没有什么特殊之处,都是常见见的,在此不再进行说明。
三、DatabaseFactory
前面介绍了mybatis用来描述列、表、数据库的三个类,那如何获得一个数据库中的这些信息呢?这就要用到这一小节中的DatabaseFactory类了。这个类只有两个函数,一个是私有的不带参数的构造函数,另一个是返回Database对象的一个静态函数。对于私有的不带参数的构造函数我们无需进行介绍,它的作用就是不让其他的类去实例化该类,我们着重了解下这个静态函数。建议读者亲自创建一个数据库、表去执行下这个函数看看返回的结果,这样会对这个函数有更深刻的理解,在这个过程中彻底的理解他人的思想,转化为自己的能力。 在这里贴出newDatabase函数的主要内容:
// 先初始化一个database对象 Database database = new Database(catalogFilter, schemaFilter); ResultSet rs = null; try { // 获取数据库连接的元数据 // ResultSet类也有getMetaData()函数,但返回的结果为ResultSetMetaData DatabaseMetaData dbmd = conn.getMetaData(); try { // 获取这个链接对应的数据库的列信息 /** * getColumns前两个参数说明,来自于该类的源代码 * * @param catalog * a catalog name; must match the catalog name as it * is stored in the database; "" retrieves those * without a catalog; <code>null</code> means that * the catalog name should not be used to narrow the * search * 如果参数值是空字符串,返回没有catalog的数据库;如果参数为null,在查询时不用这个catalog做为查询条件 * ;如果为其他值,则按照输入值进行查询。 schema的查询条件类似 * @param schemaPattern * a schema name pattern; must match the schema name * as it is stored in the database; "" retrieves * those without a schema; <code>null</code> means * that the schema name should not be used to narrow * the search * */ rs = dbmd.getColumns(catalogFilter, schemaFilter, null, null); while (rs.next()) { //获取数据 String catalogName = rs.getString("TABLE_CAT"); String schemaName = rs.getString("TABLE_SCHEM"); String tableName = rs.getString("TABLE_NAME"); String columnName = rs.getString("COLUMN_NAME"); int dataType = Integer.parseInt(rs.getString("DATA_TYPE")); //从database根据表名获取table对象,如果没有这个表,则初始化 Table table = database.getTable(tableName); if (table == null) { table = new Table(tableName); table.setCatalog(catalogName); table.setSchema(schemaName); database.addTable(table); } //将列添加到表对象中 table.addColumn(new Column(columnName, dataType)); }
四、实验
数据库服务器:mysql
数据库名:test
表结构:
CREATE TABLE `mybatis` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(20) DEFAULT '', `password` varchar(32) DEFAULT '', `sex` tinyint(1) DEFAULT '1' COMMENT '1表示男,2表示女;默认值为1', `email` varchar(30) DEFAULT '', `role` tinyint(4) DEFAULT '0' COMMENT '角色,默认值为0,表示还没有赋予角色', `status` tinyint(1) DEFAULT '1' COMMENT '1表示有效,0表示无效;默认值为1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
执行的关键代码:
String catalogName = resultSet.getString("TABLE_CAT"); String schemaName = resultSet.getString("TABLE_SCHEM"); String tableName = resultSet.getString("TABLE_NAME"); String columnName = resultSet.getString("COLUMN_NAME"); int dataType = Integer.parseInt(resultSet.getString("DATA_TYPE")); String commentString = resultSet.getString("REMARKS"); if (logger.isDebugEnabled()) { logger.debug(null); logger.debug("catalogName:" + catalogName + "; schemaName:" + schemaName + "; tableName:" + tableName + "; columnName:" + columnName + "; comment:" + commentString); }
为方便查看,去掉了log4j打印的标准前缀执行结果:
catalogName:test; schemaName:null; tableName:mybatis; columnName:id; comment: catalogName:test; schemaName:null; tableName:mybatis; columnName:username; comment: catalogName:test; schemaName:null; tableName:mybatis; columnName:password; comment: catalogName:test; schemaName:null; tableName:mybatis; columnName:sex; comment:1表示男,2表示女;默认值为1 catalogName:test; schemaName:null; tableName:mybatis; columnName:email; comment: catalogName:test; schemaName:null; tableName:mybatis; columnName:role; comment:角色,默认值为0,表示还没有赋予角色 catalogName:test; schemaName:null; tableName:mybatis; columnName:status; comment:1表示有效,0表示无效;默认值为1
请注意:输出结果中的数据 catalogName为test,而 schemaName为null。和第一小节中提到的内容不符合,也和mysql官网中的说明不符合(http://dev.mysql.com/doc/refman/5.1/zh/information-schema.html)。
有哪位高手了解原因还请告知!