如何避免编写代码

世界上最无聊的编程任务之一可能是从数据库中提取数据并在网站上显示它。然而,这也是最普遍的任务之一。Perl程序员很懒,因此有工具可以帮助减少无聊编程任务的痛苦,其中两个工具 Class::DBITemplate 工具包,其整体效果远比其各个部分更能够消除繁琐。

这两个工具可以执行比本文描述更复杂的任务,但我的目的是激励那些尚未尝试它们的人去尝试一下,看看它们能为简单的任务节省多少工作量。

我假设您已经了解了设计数据库的基本知识——为什么您有多个表以及为什么要把它们联合起来而不是将所有内容都放在同一个表中。我还假设您不讨厌阅读文档,所以我会花更多的时间来说明为什么我使用模块的特定功能,而不是详细解释它们是如何工作的。

协同作用

Class::DBITemplate 工具包之所以能够如此出色地协同工作,原因很简单。Template 工具包的模板可以调用传递给它们的对象的方法——因此,在处理模板之前,无需显式地从数据库中提取每一列——而 Class::DBI 则为您省去了编写检索数据库列的方法的麻烦。您实际上只是直接从数据库到HTML,中间只需要非常少的Perl代码。

假设您正在编写一个用于存储书籍及其作者详情以及网站用户对书籍的评论的Web应用程序。您希望有一个页面显示数据库中的所有书籍,并为每本书提供链接到所有已写评论。

  #!/usr/bin/perl -w
  use strict;

  use Bookworms::Book;
  use Bookworms::Template;

  my @books = Bookworms::Book->retrieve_all;
  @books = sort { $a->title cmp $b->title } @books;
  print Bookworms::Template->output( template => "book_list.tt",
                                     vars     => { books => \@books } );

只需为您的设计师提供一个简单的模板进行美化

  [% page_title = "List all books" %]
  [% INCLUDE header.tt %]

    <ul>
      [% FOREACH book = books %]
        <li>[% book.title %] ([% book.author.name %])
            [% FOREACH review = book.reviews %]
              (<a href="review.cgi?review=[% review.uid %]">Read review
              by [% review.reviewer.name %]</a>)
            [% END %]
        </li>
      [% END %]
    </ul>

  [% INCLUDE footer.tt %]

任务就完成了。您无需显式选择评论;您无需交叉引用另一个表以找到评论者的姓名;您无需与 HERE 文档纠缠或用 print 语句填充您的程序。您几乎什么都不用做。

当然,除了最初编写 Bookworm::* 类之外,但这很容易。

简单、小巧的类

为了方便起见,我们编写一个包含设置数据库模式所需所有SQL语句的类。这对于运行测试以及部署新安装的应用程序非常有用。

  package Bookworm::Setup;
  use strict;
  use DBI;

  # Hash for table creation SQL - keys are the names of the tables,
  # values are SQL statements to create the corresponding tables.
  my %sql = (
      author => qq {
          CREATE TABLE author (
              uid   int(10) unsigned NOT NULL auto_increment,
              name  varchar(200),
              PRIMARY KEY (uid)
          )
      },
      book => qq{
          CREATE TABLE book (
              uid           int(10) unsigned NOT NULL auto_increment,
              title         varchar(200),
              first_name    varchar(200),
              author        int(10) unsigned, # references author.uid
              PRIMARY KEY (uid)
          )
      },
      review => qq{
          CREATE TABLE review (
              uid       int(10) unsigned NOT NULL auto_increment,
              book      int(10) unsigned, # references book.uid
              reviewer  int(10) unsigned, # references reviewer.uid
              PRIMARY KEY (uid)
          )
      },
      reviewer => qq{
          CREATE TABLE review (
              uid   int(10) unsigned NOT NULL auto_increment,
              name  varchar(200),
              PRIMARY KEY (uid)
          )
      }
  );

这个类有一个单独的方法,用于设置符合上述模式的数据库。以下是它的渲染 POD;实现很简单。“force_clear”选项对于测试非常有用。setup_db( dbname => 'bookworms', dbuser => 'username', dbpass => 'password', force_clear => 0 # 可选,默认为0 );

  Sets up the tables. Unless "force_clear" is supplied and set to a
  true value, any existing tables with the same names as we want to
  create will be left alone, whether or not they have the right
  columns etc. If "force_clear" is true, then any tables that are "in
  the way" will be removed. _Note that this option will nuke all your
  existing data._

  The database user "dbuser" must be able to create and drop tables in
  the database "dbname".

  Croaks on error, returns true if all OK.

现在,另一个围绕 Template 工具包的类;我们希望从配置类中获取全局变量,例如网站名称等。(CPAN上有许多配置模块;您肯定会找到一个您喜欢的。我相当喜欢 Config::Tiny;其他人发誓说 AppConfig 很好——而且由于后者是 Template 工具包的先决条件,您已经安装了它。) Bookworms::Config 是一个简单的包装类,围绕 Config::Tiny,因此如果以后我更改配置方法,我就不必重写大量代码。

  package Bookworms::Template;
  use strict;
  use Bookworms::Config;
  use CGI;
  use Template;

  # We have one method, which returns everything you need to send to
  # STDOUT, including the Content-Type: header.

  sub output {
      my ($class, %args) = @_;

      my $config = Bookworms::Config->new;
      my $template_path = $config->get_var( "template_path" );
      my $tt = Template->new( { INCLUDE_PATH => $template_path } );

      my $tt_vars = $args{vars} || {};
      $tt_vars->{site_name} = $config->get_var( "site_name" );

      my $header = CGI::header;

      my $output;
      $tt->process( $args{template}, $tt_vars, \$output)
          or croak $tt->error;
      return $header . $output;
  }

现在我们可以开始编写管理数据库表的类了。以下是处理书籍对象的类

  package Bookworms::Book;
  use base 'Bookworms::DBI';
  use strict;

  __PACKAGE__->set_up_table( "book" );
  __PACKAGE__->has_a( author => "Bookworms::Author" );
  __PACKAGE__->has_many( "reviews",
                         "Bookworms::Review" => "book" );

  1;

是的,这就是你需要的一切。这个简单的类通过从Class::DBI的终极继承,自动创建了构造函数和访问器,用于处理我们数据库模式中定义的书籍的各个方面。更重要的是,因为我们已经告诉它(使用has_a),book表中的author列实际上是Bookworms::Author表主键的外键,当我们使用->author访问器时,实际上我们得到了一个Bookworms::Author对象,然后我们可以调用其方法。

  my $hobbit = Bookworms::Book->search( title => "The Hobbit" );
  print "The Hobbit was written by " . $hobbit->author->name;

我们需要编写几个支持类,但它们也不复杂。

首先是一个基类,就像所有Class::DBI应用程序一样,用于设置数据库细节。

  package Bookworms::DBI;
  use base "Class::DBI::mysql";

  __PACKAGE__->set_db( "Main", "dbi:mysql:bookworms",
    "username", "password" );

  1;

我们的基类继承自Class::DBI::mysql而不是普通的Class::DBI,这样我们就可以避免为每个数据库表直接指定列,数据库特定的基类将自动创建一个set_up_table方法来为你处理所有这些。

在撰写本文时,MySQL、PostgreSQL、Oracle和SQLite的基类在CPAN上可用。还有一个Class::DBI::BaseDSN,它允许你在运行时指定数据库类型。

我们还将需要为作者、评论和评论者表各创建一个类,但它们甚至比Book类更简单。例如,作者类可能就像这样简单:

  package Bookworms::Author;
  use base 'Bookworms::DBI';
  use strict;

  __PACKAGE__->set_up_table( "author" );

  1;

如果我们想能够访问特定作者的所有书籍,我们可以添加一行

  __PACKAGE__->has_many( "books",
                         "Bookworms::Book" => "author" );

并创建一个访问器,返回一个包含Bookworms::Book对象的数组,可以像这样使用

  my $author = Bookworms::Author->search( name => "J K Rowling" );
  my @books = $author->books;

或者确实

  <h1>[% author.name %]</h1>

  <ul>
    [% FOREACH book = author.books %]
      <li>[% book.title %]</li>
    [% END %]
  </ul>

简单、小巧、几乎微不足道的类,每个类只需花费几分钟来编写。

这会给我带来什么好处?

所有这些的即时好处是显而易见的。

  • 你不需要与HTML纠缠,因为非常简单的Template工具包的使用意味着模板对合格的网络设计师来说是可理解的。
  • 你不需要维护充满复制粘贴代码的类,因为重复的编程任务,如创建构造函数和简单的访问器,都是为你自动完成的。

一个巨大的潜在好处是测试。由于实际的CGI脚本(测试起来可能很痛苦)非常简单,你可以将大部分精力集中在测试底层模块上。

写几个简单的测试来确保你已经按预期设置了类,特别是在你第一次尝试使用Class::DBI的时候,这可能是有价值的。

  use Test::More tests => 5;
  use strict;

  use_ok( "Bookworms::Author" );
  use_ok( "Bookworms::Book" );
  my $author = Bookworms::Author->create({ name => "Isaac Asimov" });
  isa_ok( $author, "Bookworms::Author" );
  my $book = Bookworms::Book->create({ title  => "Foundation",
                                       author => $author });
  isa_ok( $book, "Bookworms::Book" );
  is( $book->author->name, "Isaac Asimov", "right author" );

然而,将繁重的工作从CGI脚本分离到模块中的这种技术的主要测试优势在于当你想添加更复杂的功能时。比如说,模糊匹配。众所周知,人们可能拼错字,你希望有人输入“Isaac Assimov”时能找到他们想要的作者。所以,让我们在创建作者对象时处理作者名字,并在数据库中存储某种规范化的形式。

Class::DBI允许你定义“触发器”——在对象的生存周期中的特定点被调用的方法。我们将使用after_create触发器,它在对象被创建并存储在数据库后调用。我们优先使用它而不是before_create触发器,因为我们想了解对象的uid,而这是只有在对象写入数据库后通过auto_increment主键创建的。

我们使用Search::InvertedIndex存储规范化的名字,以便快速访问。我们从一个非常简单的规范化开始——删除元音和合并重复的字母。(我发现这可以捕捉到野外约一半的名字拼写错误,这相当令人印象深刻。)

在我们继续写代码之前,我们将写一些测试。以下是一些检查我们的类是否按我们告诉它的那样做的测试——移除元音和合并重复的辅音。

  use Test::More tests => 2;
  use strict;

  use Bookworms::Author;

  my $author = Bookworms::Author->create({ name => "Isaac Asimov" });
  my @matches = Bookworms::Author->fuzzy_match( name => "asemov" );
  is_deeply( \@matches, [ $author ],
    "fuzzy matching catches wrong vowels" );
  @matches = Bookworms::Author->fuzzy_match(
    name => "assimov" );
  is_deeply( \@matches, [ $author ],
    "fuzzy matching catches repeated letters" );

我们还应该编写一些其他测试,将我们的算法运行在从实际用户那里捕获的各种错别字上,以便了解“我们告诉班级要做什么”是否是正确的。

这是对Bookworms::Author类首次增加的内容,用于存储索引数据

  use Search::InvertedIndex;

  my $database = Search::InvertedIndex::DB::Mysql->new(
                     -db_name    => "bookworms",
                     -username   => "username",
                     -password   => "password",
                     -hostname   => "",
                     -table_name => "sii_author",
                     -lock_mode  => "EX"
    ) or die "Couldn't set up db";

  my $map = Search::InvertedIndex->new( -database => $database )
    or die "Couldn't set up map";
  $map->add_group( -group => "author_name" );

  __PACKAGE__->add_trigger( after_create => sub {
      my $self = shift;
      my $update = Search::InvertedIndex::Update->new(
          -group => "author_name",
          -index => $self->uid,
          -data  => $self->name,
           -keys  => { map { $self->_canonicalise($_) => 1 }
                       split(/\s+/, $self->name)
                     }
          );
          $map->update( -update => $update );
      }
  } );

  sub _canonicalise {
      my ($class, $word) = @_;
      return "" unless $word;
      $word = lc($word);
      $word =~ s/[aeiou]//g;    # remove vowels
      $word =~ s/(\w)\1+/$1/eg; # collapse doubled
                                # (or tripled, etc) letters
      return $word;
  }

(我们还需要为after_updateafter_delete添加类似的触发器,以确保我们的索引与我们的数据保持最新。)

然后我们可以编写模糊匹配方法

  sub fuzzy_match {
      my ($class, %args) = @_;
      return () unless $args{name};
      my @terms = map { $class->_canonicalise($_) => 1 }
                        split(/\s+/, $args{name});
      my @leaves;
      foreach my $term (@terms) {
          push @leaves, Search::InvertedIndex::Query::Leaf->new(
              -key   => $term,
              -group => "author_name" );
      }

      my $query = Search::InvertedIndex::Query->new( -logic => 'and',
                                                     -leafs => \@leaves );
      my $result = $map->search( -query => $query );

      my @matches;
      my $num_results = $result->number_of_index_entries || 0;
      if ( $num_results ) {
          for my $i ( 1 .. $num_results ) {
              my ($index, $data) = $result->entry( -number => $i - 1 );
              push @matches, $data;
          }
      }

      return @matches;
  }

(匹配方法可以改进。我发现,与已经详细描述的简单方法相比,Text::SoundexText::Metaphone并没有太大的改进,但Text::DoubleMetaphone绝对值得尝试,以捕捉像Nicolas/Nicholas和Asimov/Azimof这样的错别字。)

我们的这个小型Web应用程序还有许多其他特性可以受益,但我会把这些留给读者作为练习。我希望我已经给了你一些关于我当前首选的Web开发技术的见解——如果有人感兴趣,我很乐意看到完成的Bookworms应用程序。

另请参阅

标签

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。