技术开发 频道

由浅入深:用Xcode搞定任何数据库迁移

        【IT168 技术】陆陆续续用Xcode组件将近一年了,作为一个业余开发者,很感谢大石头和他的团队。不仅感谢他们创造如此艺术的组件,更感谢他们耐心的指点,我才学会了使用模板,来开发始于自己风格和功能的通用组件。作为了老的动软代码生成器的使用者,但我接触并学会使用Xcode后,以及2年来在博客园看到的各类开发框架和ORM,我不得不说Xcode是我见过最强大的,小巧,精悍。

  很早就想写一篇教程,可能是基础比较差,写不出什么高质量的文章,毕竟是业余开发者。可能我的表述很多不专业,只有我自己理解,我也只需要对自己有用的功能。今天写这篇博文主要是受大石头的启发,因为越来越多的人使用Xcode,群里面的人都爆了,但是他们又没有太多时间来重复回答这些新手的问题,所以需要大家自力更生,看源码去学会使用。为了贡献自己对Xcode的理解和使用经验,使得后来学习的人更加容易,所以才有了我的这篇文章,写得不好,请拍砖,但拒绝人生攻击。

  1.事情起因

  NewLife.CommonEntity里面封装了一些通用实体类,今天自己做一个小工具(个人喜好,玩玩的),想得到这些通用实体类的数据字典,虽然知道有数据库反向工程的功能,但就是懒得动手。所以一开始手抄抄,然后在群里面哄了一下,然后群主大石头提示了一个CheckTables方法,其他的当然只能靠自己看源码了。其实往往很多帮助也只需要这么关键的一个提示,然后自己去学习。

  2.关于数据库方向工程:就是通过配置文件切换数据库连接字符串,不需要任何操作,系统会根据实体类自动生成数据库及数据表。很多ORM虽然可以通过配置文件修改数据库连接字符串,但还是需要在目标数据库中新建好表,因此多少有些繁琐,而Xcode彻底屏蔽了这些东西,不必要关心数据库。因此从给一个数据库转化到另外一个数据库,是再方便不过。

  3.解决过程

  在石头的提示下(CheckTables),我找到了这个方法所在的类DAL(数据访问层),看看这个方法:

1 /// <summary>检查数据表架构,不受反向工程启用开关限制</summary>
2 public void CheckTables()
3 {
4 WriteLog("开始检查连接[{0}/{1}]的数据库架构……", ConnName, DbType); 5
6 Stopwatch sw = new Stopwatch();
7 sw.Start();
8 try
9 {
10 List<IDataTable> list = EntityFactory.GetTables(ConnName);
11 if (list != null && list.Count > 0)
12 {
13 // 全都标为已初始化的
14 foreach (IDataTable item in list)
15 {
16 if (!HasCheckTables.Contains(item.Name)) HasCheckTables.Add(item.Name);
17 }
18
19 // 过滤掉被排除的表名
20 if (NegativeExclude.Count > 0)
21 {
22 for (int i = list.Count - 1; i >= 0; i--)
23 {
24 if (NegativeExclude.Contains(list[i].Name)) list.RemoveAt(i);
25 }
26 }
27 // 过滤掉视图
28 list.RemoveAll(dt => dt.IsView);
29 if (list != null && list.Count > 0)
30 {
31 WriteLog(ConnName + "待检查表架构的实体个数:" + list.Count);32
33 Db.CreateMetaData().SetTables(list.ToArray());
34 }
35 }
36 }
37 finally
38 {
39 sw.Stop();40
41 WriteLog("检查连接[{0}/{1}]的数据库架构耗时{2}", ConnName, DbType, sw.Elapsed);
42 }
43 }

  一开始看到这里,感觉不是我想要的,因为DAL里面需要传入ConnName,这里有点费解,既然要反向工程,那么数据库肯定不存在表结构,那传入ConnName有什么用?

  但是看到这但代码,我知道了,反向工程主要是通过获取实体类的IDataTable来完成的,就是上面代码第33行 Db.CreateMetaData().SetTables(list.ToArray());

  这里才是最关键的,呵呵。既然上面这个函数不满足我的要求,那就重新写一个满足要求的,所以我在DAL里面给CheckTables加了一个重载方法,如下所示:

1 public void CheckTables(List<IDataTable> list)
2 {
3 WriteLog("开始检查连接[{0}/{1}]的数据库架构……", ConnName, DbType);
4 Stopwatch sw = new Stopwatch();
5 sw.Start();
6 try
7 {
8 if (list != null && list.Count > 0)
9 {
10 WriteLog(ConnName + "待检查表架构的实体个数:" + list.Count);
11 Db.CreateMetaData().SetTables(list.ToArray());
13 }
14 }
15 finally
16 {
17 sw.Stop();
18 WriteLog("检查连接[{0}/{1}]的数据库架构耗时{2}", ConnName, DbType, sw.Elapsed);
19 }
20 }

  有了上面的这个函数,只需要把自己想要反向工程的实体类列表传入进去就可以了,下面是我的调用方法:

1 List<IDataTable> list = new List<IDataTable>();
2 list.Add(Administrator.Meta.Table.DataTable);
3 list.Add(Area.Meta.Table.DataTable);
4 list.Add(Category.Meta.Table.DataTable);
5 DAL dal = DAL.Create("Common");
6 dal.CheckTables(list);

  简单的几行代码,就将 NewLife.CommonEntity中的几个表的结构自动生成到数据库了。我印象中记得,可以一下子获取 NewLife.CommonEntity所有的 List ,一时也想不起来,所以就只能一个个添加,呵呵,先解决实际问题,其他的再慢慢来搞。

${PageNumber}

  4.继续分析,虽然上面解决了的实际问题,但没有对反向工程的整个过程有更深入的了解。而且也萌生了念头,如此方便,那岂不是很容易的将数据库进行迁移,表结构和数据都很容易迁移到其他数据库平台了,正好闲着没事,写了一段简单的代码,调试看看【刚开始调试了半个小时,马马虎虎,有点晕,晚上回来继续奋战3个小时,才有点眉目,也难怪,大石头的团队花了好多年的成果,我怎么可能这么短时间就消化掉】。代码很简单,新建一个管理员,赋值并插入数据,没有数据库:

1 Administrator user = new Administrator();
2 user.Name = "admin";
3 user.Password = DataHelper.Hash("admin");
4 user.DisplayName = "超级管理员";
5 user.RoleID = 1;
6 user.IsEnable = true;
7 user.Insert();

  断点调试上述代码,记下主要内容吧:

  1.在new对象的过程中,会调用基类的构造函数 TEntity entity = new TEntity();然后字段赋值

  2.在对象插入到数据库中的过程中(user.Insert())会调用基类的 Insert()方法,在此方法中,有一个Valid(Boolean isNew)方法用来验证实体的数据是否符合要求,在检查实体数据过程中,会到数据库中获取一定的信息,如果数据表不存在,就会新建数据库和相应的表结构。这一部分涉及到的类挺多的,我也有些晕。大体如下吧,可能会有理解不到位:

  2.1 为了验证数据,需要判断该数据是否在数据表中已经存在,所以有了Entity.cs中的CheckExist方法,其中有一个FindCount(names, values)去数据库获取满足记录的条数。

  2.2 然后获取查询条件,调用Entity.Meta中的QueryCount方法,然后QueryCount调用WaitForInitData方法进行 检查并初始化数据,这时会对实体类进行检查。

  2.3 实体类第一次检查模型时,会调用Entity.Meta中的CheckModel()方法,这其中会对表结构的ModelCheckModes进行检查,ModelCheckModes分为2种情况,1种是初始化时反向工程,一种是使用时反向工程。Xcode连这一点多考虑到了,说明是多么的细致,因为在我看来即使是初始化时进行,也没有多少时间。TableItem有一个ModelCheckMode属性,会去获取实体类的ModelCheckMode,这个实现用到了Attribute,如果实体类声明了ModelCheckModeAttribute,那么获取到的就是这个值,如果没有声明,就是CheckAllTablesWhenInit。

  2.4 会把需要初始化化的表结构添加到List中,然后调用DBO.Db.CreateMetaData().SetTables(Table.DataTable),这就是最上面提到的那段关键代码,下面进这个方法看看。

  2.5 SetTables方法中先对数据库进行检查( CheckDatabase()),然后检查指定的数据表(CheckAllTables(tables))

  2.5.1 数据库检查时,如果数据库不存在,会调用SetSchema(DDLSchema.CreateDatabase, null, null)创建数据库(杯具的我,一边调试一边看数据库里面有没有生成东西,来回看,到这里才真的生成数据库了,表还没有)。

  2.5.2 数据表检查时CheckAllTables(tables),这里面会去判断对应的表是否存在数据库,如果不存在就创建表,否则修改表结构。调用CheckTable对单个表进行检查,创建表主要就是CreateTable方法了,运行完CreateTable,数据表Administrator也在数据库中出来了。

  5.改进。写了这么多,虽然对整个过程的很多细节还不是很透彻,这篇博客开始写的时候已经调试了3个小时左右,等写完又基本上调试了2,3个小时,一遍调试一边写。写到最后,我也对知道应该如何更好的调用方向工程方法了,我最先的调用方法也可以,但是不具有通用性。上面也提到我记得可以一次获取所有的实体类,可能是开始太粗心,对有些过程不了解,认为EntityFactory.LoadEntities方法不能获取到所有的实体类,但实际上应该是可以的。这其中有点小风波:

  刚开始石头提醒我用CheckTable,我就发现了EntityFactory有GetTables方法,所以我写了这句

  List list = EntityFactory.GetTables("Common");

  但奇怪的是这样获取到的List是空的,纳闷了很久,最终放弃了,然后用上面的方法,先解决问题再说。等我快写完博客,我才发现其中的原因,因为当初没继续往下面调试啊。因为默认是“不从未加载程序集中获取类型”,因为我项目应用的是dll,那么应该需要加载程序集才行。所以我写了下面的一个方法:

1 /// <summary>
2 /// 获取指定连接名下的所有实体数据表
3 /// </summary>
4 /// <param name="connName">数据库连接名称</param>
5 /// <param name="isLoadAssembly">是否加载外部程序集</param>
6 /// <returns>IDataTable集合</returns>
7 public static List<IDataTable> GetTables(String connName,bool isLoadAssembly)
8 {
9 var tables = new List<IDataTable>();
10 // 记录每个表名对应的实体类
11 var dic = new Dictionary<String, Type>(StringComparer.OrdinalIgnoreCase);
12 var list = new List<String>();
13 IEnumerable<Type> Cur = AssemblyX.FindAllPlugins(typeof(IEntity),isLoadAssembly).Where(t => TableItem.Create(t).ConnName == connName);
14 foreach (Type item in Cur )
15 {
16 list.Add(item.Name);
17 // 过滤掉第一次使用才加载的
18 var att = ModelCheckModeAttribute.GetCustomAttribute(item);
19 if (att != null && att.Mode != ModelCheckModes.CheckAllTablesWhenInit) continue;
20 var table = TableItem.Create(item).DataTable;
21 //判断表名是否已存在
22 Type type = null;
23 if (dic.TryGetValue(table.Name, out type))
24 {
25 // 两个实体类,只能要一个当前实体类是,跳过
26 if (IsCommonEntity(item))
27 continue;
28 // 前面那个是,排除
29 else if (IsCommonEntity(type))
30 {
31 dic[table.Name] = item;
32 // 删除原始实体类
33 tables.RemoveAll((tb) => tb.Name == table.Name);
34 }
35 // 两个都不是,报错吧!
36 else
37 {
38 String msg = String.Format("设计错误!发现表{0}同时被两个实体类({1}和{2})使用!", table.Name, type.FullName, item.FullName);
39 XTrace.WriteLine(msg);
40 throw new XCodeException(msg);
41 }
42 }
43 else
44 {
45 dic.Add(table.Name, item);
46 }
47
48 tables.Add(table);
49 }
50
51 if (DAL.Debug) DAL.WriteLog("[{0}]的所有实体类({1}个):{2}", connName, list.Count, String.Join(",", list.ToArray()));
52
53 return tables;
54 }

  然后在主程序中调用 List list = EntityFactory.GetTables("Common");然后一句话我要的东东都有了。呵呵,其他的就不说了。

  上面主要是增加了一个参数:IEnumerable Cur = AssemblyX.FindAllPlugins(typeof(IEntity),isLoadAssembly).Where(t => TableItem.Create(t).ConnName == connName);

  呵呵,建议石头把这个参数补充上去,因为也不费事,要是在4.0里面,来个默认参数。

  下面是大石头对反向工程的说明:所有实体,默认情况下,根据连接名分组,只要用到这个实体,那么跟这个实体在同一个连接名旗下的所有实体,都会开始进行反向工程,建立数据库数据表。

  除了CheckAllTablesWhenInit的实体,标记CheckAllTablesWhenInit的实体,主要位于CommonEntity,它们是用到之后,只会为自己这个实体创建数据表。

  因为CommonEntity里面所有实体都在Common这个连接名下,然后很多时候只需要用到CommonEntity里面的部分表,而不是全部表。

${PageNumber}

  1、为什么要向数据库随机插入数据

  今天写程序,用昨天的数据库反向工程生成了数据库,但是里面没有数据,又懒得动手去手动插入。想写一个程序随机插入测试数据,当然写之前又在群里面喊了一下,看有没有人已经做过,可是一群友说,原话不记得了,反正意思就是说:"这么简单还来问,自己For一个不就行了"。当然这个群友说得没错,但是说实话 这个想法太幼稚了点,一个程序员或者开发人员每次想到问题,不是去抽象,不是尽量去做得通用点,每次都重复的For,那也太悲催了。虽然我不是很专业开发人员,但是我还是想写一个通用点的程序,来方便的插入随机数据,而不是每一次,每一个表都去For。

  2.功能要求

  1).使用Xcode,强大就不用说了,可以完全屏蔽数据库的差异,而且也很容易对数据库架构信息进行操作 ;

  2).要求只需要 ”使用数据库连接字符串“,和插入数据条数,就可以进行数据插入,不需要实体类,当然你的数据库已经存在了(不存在就先设计好,或者说反向工程建好);

  3).要求更加字段类型和长度,随机插入对应类型的数据

  3.开发过程

  其实上面要求挺简单的,可能大家认为主要对第2个要求比较难,因为只需要数据库连接字符串和数据库就来进行,当然需要Xcode.下面直接贴代码吧,代码里面有注释:

  按照大石头提出的意见,对程序进行了修改:

1 /// <summary>
2         ///  随机填充指定数据库连接字符串中的所有表
3         /// </summary>
4         /// <param name="connStr">数据库连接字符串</param>
5         /// <param name="needCount">填充的记录数目</param>
6         public static void FillDataForDb(string connStr, int needCount = 50)
7         {
8             DAL dal = DAL.Create(connStr);//根据数据库连接字符串创建数据访问对象
9             List<IDataTable> tableList = dal.Tables;//获取数据库的所有表和架构信息
10             tableList.RemoveAll(t => t.IsView);//过滤掉视图
11             foreach (var item in tableList)
12             {
13                 //首先根据表名称获取当前表的实体操作接口
14                 IEntityOperate entity = dal.CreateOperate(item.Name);
15                 for (int i = 0; i < NeedCount; i++)
16                 {
17                     IEntity model = entity.Create();//创建数据实体接口
18                     FieldItem[] filds = entity.Fields;//获取所有的字段信息
19                     foreach (var fild in entity.Fields)
20                     {
21                         if (!fild.IsIdentity)
22                             model.SetItem(fild.Name, GetRandomValue(fild));
23                     }
24                     model.Save();//保存数据
25                 }
26             }
27         }

  详细的不解释了,有什么问题,大家到论坛去提问,论坛地址在后面。主要一个说明的是有一个GetRandomValue(FieldItem fild) 函数,它会根据字段信息来获取对应类型的随机值,这段代码比较简陋,先贴出来吧,感觉应该有更简单的方法去操作,希望知道的人提醒一下。【果然,本来开始用Switch的,结果不支持Type,原来是自己用错了,大石头提出来,呵呵,我顺便更新了】

1 /// <summary>
2         /// 根据字段类型和长度获取对应类型的随机数据
3         /// </summary>
4         /// <param name="fild">字段对象</param>
5         /// <returns>对应的随机数据</returns>
6         public static object GetRandomValue(FieldItem fild)
7         {            
8             switch (Type.GetTypeCode(fild.Field.DataType))
9             {
10                 case TypeCode.Boolean: return RandomHelper.GetRandomBool();
11                 case TypeCode.Byte:return RandomHelper.GetRandomByte();
12                 case TypeCode.Char:return RandomHelper.GetRandomChar();                
13                 case TypeCode.DateTime:return RandomHelper.GetRandomDateTime();
14                 case TypeCode.Decimal:return RandomHelper.GetRandomDouble(0, NeedCount*10.1);
15                 case TypeCode.Double:return RandomHelper.GetRandomDouble(0, NeedCount*10.1);              
16                 case TypeCode.Int16:return RandomHelper.GetRandomInt(1,UInt16.MaxValue );                  
17                 case TypeCode.Int32:return RandomHelper.GetRandomInt(1,NeedCount *50);      
18                 case TypeCode.Int64:return RandomHelper.GetRandomInt(1,NeedCount*100);                                    
19                 case TypeCode.SByte:return RandomHelper.GetRandomInt(1,127);
20                 case TypeCode.Single:return RandomHelper.GetRandomDouble(0, NeedCount*10.1);
21                 case TypeCode.String:return RandomHelper.GetRandomString((int )(fild.Length*RandomHelper.GetRandomDouble (0.2,0.7)));
22                 case TypeCode.UInt16:return RandomHelper.GetRandomInt(1,UInt16.MaxValue );                    
23                 case TypeCode.UInt32:return RandomHelper.GetRandomInt(1,NeedCount *50);                          
24                 case TypeCode.UInt64:return RandomHelper.GetRandomInt(1,NeedCount*100);
25                 default:
26                     return string.Empty;
27             }

  RandomHelper这个常用类大家都有,也比较简单,就不贴了,就是这个思路。

  4.改进

  如果说改进,那就是要是能够插入一些更人性化的数据就好了,呵呵,以后再考虑。写起来很简单,当然也要Xcode的支持。

${PageNumber}

  使用Xcode进行了数据库反向工程以及自动向数据库插入测试数据的功能,其实是为了这个东西-数据库迁移工具。利用测试数据,将利用Xcode编写一段代码,进行数据库之间的转换和迁移,不仅包括数据库架构,同时包括数据库中的数据记录。下面记录一下开发过程:

  1.为什么要进行数据库迁移

  这是一个很常见的需求。现在数据库众多,有时候开发和部署的数据库不一样,或者说在软件运行过程中,由于某些原因要换数据库等得。对于程序员来说提到切换数据库,可能对一部分人来说是噩梦,虽然很多工具实现了通过切换配置文件,来切换数据库,但仍然需要建立好数据库才行,或者利用SQL语句,先在新的数据库平台建立数据库。这样程序是不变,但数据库还是有变化,还得动手去数据库做点东西。那么使用Xcode的目的是什么,就是利用Xcode,不用在数据库做任何东西,包括新的数据库,只需要改变一个配置文件即可。

  2.功能要求

  1).同样是要使用Xcode,前面博客已经实现了一些数据库操作的功能,如数据库反向工程,插入随机数据等。

  2).不需要手动在外部数据库平台做任何事情,只需要修改数据库连接字符串,以及为了考虑转换速度,设置几个参数。

  3).适用于任何2种数据库之间的迁移转换,据我所知,目前支持的数据库有:MSSQL,MySQL,Oracle,Access,Sqlite,PostgreSQL,Firebird等等,当然还有接口,给你最大的自由支持其他网络数据库。

  4).数据库迁移包括数据库架构(表、字段等全部信息)和所有数据记录。

  5).不需要生成实体类。

  6).下一步开发数据库备份功能(备份数据库架构和数据记录),这样备份的数据可以导入到任何数据库中(这一点是不是也很强悍),这不是简单的导出SQL语句,毕竟导出的SQL,数据库之间通用性不那么高。

  这些东西还不够吸引你吗?而上面的前5个功能,在Newlife.Xcode的支持下只需要短短20行代码。呵呵,有点嘘头,当然对上千万条数据的数据库还要考虑性能,这里面也只稍微考虑一下。看看过程吧。

  3. 开发过程

  首先开发思路,如果了解前2篇博客,就会很容易得到这个思路:利用Xcode获取源数据库的架构信息,然后利用反向工程在新数据库平台建立数据库(迁移架构信息),然后对每个表,获取实体操作接口,并批量分页获取数据,并将这些数据插入到新的数据库表中。很简单,前后实现代码20行左右。如下所示,有注释:

1 /// <summary>
2 /// 拷贝数据库,只需要数据库连接字符串和源数据库即可
3 /// </summary>
4 /// <param name="originConn">源数据库连接字符串</param>
5 /// <param name="desConn">目的数据库连接字符串</param>
6 /// <param name="perCount">每次获取的记录数目,如果默认-1则会自动调用函数计算一个合理值</param>
7 public static void CopyDataBase(string originConn,string desConn,int perCount = -1)
8 {
9 //思路:通过源数据库获取架构信息,然后反向工程,然后导出数据
10 DAL dal = DAL.Create(originConn);
11 List<IDataTable> tableList = dal.Tables;//获取源数据库的架构信息
12 tableList.RemoveAll(t => t.IsView);//过滤掉视图
13 //首先拷贝数据库架构
14 DAL desDal = DAL.Create(desConn);//要在配置文件中启用数据库架构才行
15 desDal.Db.CreateMetaData().SetTables(tableList.ToArray());
16 //然后依次拷贝每个表中的数据
17 foreach (var item in tableList)
18 {
19 //首先根据表名称获取当前表的实体操作接口
20 IEntityOperate Factory = dal.CreateOperate(item.Name);
21 //分页获取数据,并更新到新的数据库,通过更改数据库连接来完成
22 int allCount = Factory.FindCount ();
23 if (perCount < 0) perCount = GetDataRowsPerConvert (allCount );
24 int pages = (int)Math.Ceiling ((double)((double )allCount/(double )perCount));
25 for (int i = 0; i < pages ; i++)
26 {
27 Factory.ConnName = originConn;
28 IEntityList modelList = Factory.FindAll(string.Empty, string.Empty, string.Empty, i * perCount, perCount);
29 Factory.ConnName = desConn;
30 modelList.Insert(true);
31 }
32 Console.WriteLine("数据库{0} 数据转移完成!",item.Name );
33 }
34 }
35
36 /// <summary>
37 /// 根据数据表的记录总数来设置一个合理的每次转换数目。数据量大,一次性导出导入不合理
38 /// </summary>
39 /// <param name="allCount">数据表记录总数</param>
40 /// <returns>每次转换的记录数</returns>
41 private static int GetDataRowsPerConvert(int allCount)
42 {
43 if (allCount < 1000) return 200;
44 else if (allCount < 5000) return 500;
45 else if (allCount < 50000) return 1000;
46 else return 1500;
47 }

  实现上面功能时出了点小问题,导致数据插入不了新数据库,因为我使用的是Save(),而不是Insert(),这2个东西还是有点差别,呵呵,多调试了2个小时,最后还是石头指出来的。

  上面代码我测试了一下,从MSSQL 2005 迁移到 Mysql5.1,顺利完成。只测试了300条数据。也没有出现任何错误。应该算是没什么大问题,小问题的话,需要的人拿过去,修改下。这里只提供个思路,可以集成到自己的开发工具和应用软件中去,如加一个异步处理,多线程之类的。下一步打算做数据库备份,就是上面要求的最后一个,这个对于小型数据库来说,还是比较容易的,关键是大数据库有点难办,希望大家也给点意见啊。

  4.改进

  这几篇博客都只是一个思路,从中可以看出Xcode操作数据库的便利性和其通用性。数据库迁移功能已经实现了最基本的功能(转移数据库架构和数据记录),但运行效率,大量数据的运行情况等都需要完善。另外,有了这个思路,可以做数据库备份,而且数据库备份数据库,可以导入到任何数据库中。呵呵,想想是不是这么回事?这个功能也应该花不了多少时间。有时间继续做做。

  说明一下,本文使用的Xcode不是Mac的Xcode,而且Newlife团队开发的一个.NET开发组件。其历史也有将近10年,因此大家不要误会。

  新生命开发团队的相关信息:

  QQ群:1600800

  博客:http://nnhy.cnblogs.com

  论坛:http://www.53wb.com

  开源地址:http://xcode.codeplex.com/

0
相关文章