前面一片文章讲了如何将Access数据库直接导入SQL Server。但导入完成后,是强烈建议不要使用的。因为里面的object命名都与sql默认的不符,许多T-SQL行为也很诡异,所以我需要重新建立一个数据库,并且把旧的数据导入到新的数据库中。

新数据库的设计


对于新的数据库,这恰好是一次重构和升级的机会,我可以将4年前不合理的表结构设计重构一下(4年前也就是我第一次做这个博客的时候,菜鸟一只)。新的数据库中,博客子系统的设计如下(其实还有很多别的表,但不是给博客主业务用的,以后再讨论):

而旧数据库仅仅只有3张表:Blog、Category、Comment。

对于Category,没有太大的改动,仅仅是增加一个”Name”字段,用来做MVC的路由,而”DisplayName”的功能则和原先数据库是一样的,表示这个分类在页面上显示的名称。

Comment表增加了Email字段,是可选的,用来保存评论者的邮件地址,以便我和他联系。

Blog表重命名为了Post,这是因为概念的转变。Blog应当是系统的总称,里面可以有文章、有留言、以及友情链接、设置等其他数据。而Post,则是描述所有的文章,是博客的主业务对象。

新增的表是Tag、PostTag及BlogAttachment。原先文章的标签都是在Blog.Keywords中以逗号分隔的,由网站程序负责检索和分割。这样显然不符合数据库设计规范(1NF),并且很影响效率。因此我单独抽了两张表,Tag用于保存标签名称。PostTag用于做多对多的关联(一篇文章可以对应很多个标签,而一个标签可以被许多文章使用)。

BlogAttachment保存的是文章的附件。这个功能最早也是在数据库里的,但后来交给了XML管理,其实这样做不太合理,所以这次又把它重新放回数据库了。

如何导入数据


建立了新数据库以后,下一步就是要把数据从老的数据库导入到新数据库了。这里面会存在许多要解决的问题。其中一个就是表结构不匹配的问题。如果只是字段名不一样,可以直接在INSERT SELECT语句中替换,而如果结构不一样,就要进一步处理了。

导入的时候也要注意,要从主表开始导数据,再导从表的数据(外键约束很明显)。

在SQL Server中,要跨数据库访问很简单,只要在要查询的对象前面加上数据库名就可以了。比如SELECT * FROM ABC.dbo.Table1,就算这条语句是在DEF数据库执行的,只要你拥有对ABC数据库的访问权限,就可以查询成功。

首先,最容易处理的是分类表(Category),脚本如下:

INSERT INTO Category
  (
    NAME,
    DisplayName
  )
SELECT c.CategoryName,
       c.CategoryName
FROM   BlogSQL.dbo.Category c

这个脚本会把新表的Name和DisplayName都用旧表的CategoryName来填充,新表的主键Id是自从产生的GUID(这不影响Category的导入,但对于从表Post来说,就需要额外处理CategoryId外键)。而更改Name的工作很明显,需要手动完成(毕竟没有哪个数据源中保存了Name)。

接下来要处理的是最Post表。这里有几个问题需要处理。首先,CategoryId不再能直接导入了,因为数据类型从int变成了uniqueidentifier。但我们还是能根据Category的DisplayName找到对应的一个Category,因为DisplayName是不会重复的,没有两个相同的分类。然后再取他的Id。这只需要两句Inner Join就搞定。

INNER JOIN BlogSQL.dbo.Category c
            ON  c.CategoryId = b.CategoryId
INNER JOIN EdiBlog.dbo.Category c2
            ON  c2.DisplayName = c.CategoryName

另外,同样的,旧数据库中文章的Id为int类型,而新数据库中是uniqueidentifier,即GUID。单单插入数据是不影响的,GUID可以用NEWID()自动产生,但在Post的从表Comment中,就会多出一个额外需要处理的问题,即Comment的外键PostId也不能直接导入了。

最后一个问题是,Access数据库中没有GUID对应的类型,因此我保存的都是空GUID作为替代。这个只要在最后将空ID字符串替换成NULL就好了。导入Post的脚本如下:

INSERT INTO Post
  (
    Title,
    Slug,
    PubDate,
    PostContent,
    Author,
    CommentEnabled,
    AttachmentId,
    IsPublished,
    Hits,
    Rators,
    Rating,
    CategoryId
  )
SELECT b.Title,
       b.Slug,
       b.Posttime,
       b.[Content],
       b.Author,
       b.IfReply,
       b.AttachmentId,
       b.IsDraft,
       b.Hits,
       b.Rators,
       b.Rating,
       c2.Id
FROM   BlogSQL.dbo.Blog b
       INNER JOIN BlogSQL.dbo.Category c
            ON  c.CategoryId = b.CategoryId
       INNER JOIN EdiBlog.dbo.Category c2
            ON  c2.DisplayName = c.CategoryName

UPDATE EdiBlog.dbo.Post SET AttachmentId = NULL WHERE AttachmentId = '00000000-0000-0000-0000-000000000000'

下一个表,是Post的从表Comment。和之前处理类型外键一样,用两个Inner Join搞定:

INSERT INTO Comment
  (
    Username,
    Email,
    IPAddress,
    PubDate,
    CommentContent,
    IsPublished,
    PostId
  )
SELECT c.Replyer,
       NULL,
       c.IPAddress,
       c.Posttime,
       c.ReplyContent,
       '1',
       p.Id
FROM   BlogSQL.dbo.Comment c
       INNER JOIN BlogSQL.dbo.Blog b
            ON  b.Id = c.BlogId
       INNER JOIN EdiBlog.dbo.Post p
            ON  p.Title = b.Title

最后要处理的是标签库,即Tag和PostTag两张表。这是本次升级中最麻烦的导入,必须另起一片文章详细论述。所以这篇就不讨论标签库了。有兴趣的读者请留意下一篇文章。

新的存储过程


原来用Access的时候,所有的业务操作都是放在程序中的,很影响效率。我的博客是一个internet站点,需要面对大量的并发访问,这些request并不全是人类,还有搜索引擎爬虫及其他内容检索程序,因此效率很关键。而SQL Server的一大好处就是可以利用存储过程,直接把耗时的逻辑在数据库层面解决,因此我就利用了这一特性,将一些业务操作重写到了存储过程里。下面挑选一些介绍。

GetCategoryCountList

这个存储过程做的是统计每个分类下有多少文章。用到了GROUP BY以及COUNT。

CREATE PROCEDURE [dbo].[GetCategoryCountList]
AS
BEGIN
    SELECT [Category].Name,
           [Category].DisplayName,
           COUNT(Post.Id) AS PostCount
    FROM   [Category]
           LEFT JOIN [Post]
                ON  [Post].CategoryId = [Category].Id
    GROUP BY
           [Category].Name,
           [Category].DisplayName
END

执行结果如下:

GetTagCountList

和刚才类似,也是做统计的。它会返回每个标签对应了多少文章。

CREATE PROCEDURE [dbo].[GetTagCountList]
AS
BEGIN
    SELECT t.Name            AS TagName,
           COUNT(pt.PostId)  AS TagCount
    FROM   Tag                  t,
           PostTag              pt
    WHERE  t.Id = pt.TagId
    GROUP BY
           t.Name
END

效果如下:

GetMonthList

返回每年各月份所发表的文章数量。用于做归档页面。

CREATE PROCEDURE [dbo].[GetMonthList]
AS
BEGIN
    SELECT YEAR(p.PubDate)   AS QYear,
           MONTH(p.PubDate)  AS QMonth,
           COUNT(p.Id)       AS QCount
    FROM   Post                 p
    GROUP BY
           YEAR(p.PubDate),
           MONTH(p.PubDate)
END

效果如下:

另一些不重要的存储过程就不介绍了。下一篇将介绍标签库的数据导入。