本文系今天检查一段代码,有感而发。真实需求、真实代码。本文涉及的技术:数据库、ORM、Entity Framwork,但是只要有一点SQL知识即可理解。

假设有这么一个需求:

在一个图书相关的系统中,有很多用户和图书,图书有作者和译者。比如说,有一个用户,他的名字叫做 Admin,他以张一、赵一和赵九作为不同的笔名出版了7本书,同时作为译者还出版了4本书,我们的程序需要显示如下的这样一个列表。需要显示如下的数据模型:

enter image description here

数据模型也很简单:

有三个基本对象:Book(图书)、User(系统中的用户)、Contributor(作为笔名的实体)。

他们之间的关系是:

  • User和Contributor之间是一对多关系:每个Contributor对应于一个User,一个User对应于多个Contributor(一个人可以有多个笔名).
  • Book和Contributor之间是多对多关系(一本书有多个作者,一个作者可以写多本书)。

因此,Contributor和User之间通过外键关联,而Book和Contributor之间则需要一个映射表。模型定义如下:

public class Contributor
{
    public int Id { get; set; }
    public string Name { get; set; }

    public int UserId { get; set; }
    public virtual ApplicationUser User { get; set; }
}

public class BookContributorMapping
{
    public int BookId { get; set; }
    public Book Book { get; set; }

    public int ContributorId { get; set; }
    public Contributor Contributor { get; set; }

    public ContributorType ContributorType { get; set; }
}

有了上面的代码,不难对应到的SQL数据表上了,每个对象一个数据表。

enter image description here

好了,现在问题来了!

应该如何获取上面图中显示的列表。这个问题确实很简单,稍稍学过一点 SQL 的人都能做出来。

用什么技术都是一样的,这里是用 .NET 工具栈,所以用了 Entity Framworks,这是一个ORM。本质上就是从数据库里,把需要的数据获取出来,然后显示到页面上。

现在来看一个程序员写出的真实代码:

      var  viewModel = new ..... //创建视图模型

        viewModel.Authors = new Dictionary<string, ICollection<string>>();
        viewModel.Translators = new Dictionary<string, ICollection<string>>();

        foreach (var contributor in user.Contributors)
        {
            foreach (var authorBookContributor in contributor.BookContributorMappings
                .Where(bc => bc.ContributorType == ContributorType.作者))
            {
                var authorName = authorBookContributor.Contributor.Name;
                if (!viewModel.Authors.ContainsKey(authorName))
                {
                    viewModel.Authors[authorName] = new List<string>();
                }

                viewModel.Authors[authorName].Add(
                    authorBookContributor.Book.Name);
            }

              //同样方法处理译者列表 
              //......
        }

我们先看来看看这位程序员的思路:

      var  viewModel = new ..... //创建视图模型

        viewModel.Authors = new Dictionary<string, ICollection<string>>();
        viewModel.Translators = new Dictionary<string, ICollection<string>>();

        //user是当前需要显示的用户对象,用for循环遍历他的所有Contributor
        foreach (var contributor in user.Contributors)
        {
            //然后找到某一个contributor对应的所有作者映射
            foreach (var authorBookContributor in contributor.BookContributorMappings
                .Where(bc => bc.ContributorType == ContributorType.作者))
            {
               //得到笔名
                var authorName = authorBookContributor.Contributor.Name;

               //在结果集合中,如果给还没有出现过这个笔名就增加一项
                if (!viewModel.Authors.ContainsKey(authorName))
                {
                    viewModel.Authors[authorName] = new List<string>();
                }

                //在这个笔名对应的图书列表中,加入该映射对应的图书的名字 
                viewModel.Authors[authorName].Add(
                    authorBookContributor.Book.Name);
            }

              //同样方法处理译者列表 
              //......
        }

看起来也完成了任务。

好了,问题又来了:

内存中的问题就忽略不计了,只考虑访问数据库的情况,需要访问多少次数据库?

双重循环,假设一个User有n个笔名,每个笔名有m本书出版,完成上面的过程就需要 m x n 次数据库访问。

很多文章说ORM性能有问题,如果这么用ORM当然有问题,用这种写法,即使使用SQL,性能还是一样不堪啊。关系数据最牛逼的一点是什么? Join啊,Join去哪儿了?

好了,问题又来了:

正确的方法,应该怎么办?

viewModel.Authors = db.BookContributorMappings
    .Where(m => m.Contributor.UserId == id && m.ContributorType == ContributorType.作者)
    .Select(m => new { ContritutorName = m.Contributor.Name, BookName = m.Book.Name})
    .GroupBy(m => m.ContritutorName)
    .ToDictionary(g => g.Key, g => g.Select(m => m.BookName));

wow~~ 代码短了好多?长短不是关键,关键在于两点:

1)先说和性能无关的可读性:

看这种代码不动脑子,一眼就能知道代码执行的逻辑,简直不像再写程序,而是像在和人说话:

viewModel.Authors = 
    //从Book和Contributor之间的映射表开始
    db.BookContributorMappings
    //找到该用户作为作者的所有映射
    .Where(m => m.Contributor.UserId == id && m.ContributorType == ContributorType.作者)
    //只保留需要的数据列
     .Select(m => new { ContritutorName = m.Contributor.Name, BookName = m.Book.Name})
    //根据Contributor分组          
    .GroupBy(m => m.ContritutorName)
    //执行SQL查询把结果填入内存
    .ToDictionary(g => g.Key, g => g.Select(m => m.BookName));

2)效率:

除了最后一行,前几行都是在构造查询,到最后一步,把查询翻译成SQL语句,然后一次性从数据库中数据读出来,装入内存,而且仅仅从数据库中获取需要的两列(作者笔名和书名),其他列的数据都不碰。 性能杠杠的,当然如果还嫌不够,还可以压榨性能,这里就不哆嗦了。

关键在于仅仅一次数据库访问就可以把所有数据获取到!

我们知道,数据库访问时大多数系统性能问题的关键,m x n 和1 比起来,性能差有多大呢。(当然对这里,m和n都是个位数,并不明显。但是不往多里说,只要把m和n都放大到两位数,一个页面几千次数据库访问,就得疯了。)

结论

作为程序员,要清楚一点,只有1%的程序员解决的是真正难的问题,剩下99%程序员解决的都是非常简单的问题(这不是我说的,这是图灵奖得主说的),只要你能扎扎实实真正把基础知识理解好,工具掌握好,就足够了。

作为解决简单问题的程序员,要有一点大局观,至少对“复杂度”的数量级要有点观念,差个常数级别的性能差异无所谓,但是 O(n^2) 和 O(1) 的差异就不能忍了。

基本功啊,基本功……