数据库完整性和 Web 应用程序

想要提高数据的完整性吗?请在数据库和应用程序中设置约束。

NoSQL,非关系型数据库的统称,在 Web 开发人员中风靡一时。然而,用 NoSQL 这个术语来描述它们有些不公平且无济于事,因为其中涉及的技术种类繁多。即便如此,传统关系型数据库和它们的 NoSQL 对等物之间还是存在一些根本区别。首先,顾名思义,NoSQL 数据库不使用标准的 SQL 查询语言,而是使用它们自己的类 SQL 语言(例如 MongoDB)或面向对象的 API。另一个区别是缺少二维表;SQL 数据库完全使用这种表进行操作,而 NoSQL 数据库则避开它们,转而支持名称-值对或类似哈希的对象。最后,NoSQL 数据库通常缺乏导致关系型数据库发展的特性,即事务和数据完整性。

毫无疑问,NoSQL 数据库提供的灵活性在许多层面上都具有吸引力。正如我喜欢使用动态语言,在这些语言中,我不需要在使用变量(或它们的类型)之前声明它们,能够在我的数据库中存储对象而无需预先定义对象结构也很不错。如果我想向我的 Person 对象添加一个新字段,我只需这样做,数据库就会神奇地赶上。

与此同时,在很多情况下,我希望数据库对我严格要求,并强制执行我的数据完整性。也就是说,我希望确保即使我在应用程序中犯了错误,或者用户输入了不应该允许的值,数据库也不会允许存储这些错误数据。是的,我相信在数据库级别进行此类检查是好的,而不仅仅是在应用程序级别——不仅因为它提供了额外的保护来防止数据损坏,还因为数据库通常可以直接在应用程序外部访问。我经常需要对为我的客户运行的应用程序进行我所说的“数据库手术”,而且知道我不能手动进行会损坏数据的更改总是令人安心的。

我专栏的读者知道我是 Ruby on Rails 和 PostgreSQL 的粉丝,而且我经常在项目中使用它们。然而,由于 Rails 最初是在 MySQL 下开发的,而 MySQL 缺乏 PostgreSQL 的许多数据完整性方面,标准的 Active Record 包未能在其基本实现中包含外键等项目。这意味着虽然 Rails 开箱即用地支持 PostgreSQL,但它不提供对外键的支持,更不用说数据完整性检查了。

因此,在本文中,我将介绍一些 Ruby gem,它们使在您的 Active Record 模型中包含外键支持成为可能。我还将描述您可以利用 PostgreSQL 的 CHECK 机制来确保您的数据尽可能安全的方法。

您在哪里检查数据?

在深入实际代码和实现之前,我承认关于在何处以及如何检查数据存在几种哲学。正如我之前提到的,我更喜欢在数据库和应用程序级别都检查数据,尽管有时我很懒,使用了 Rails 中的默认设置,即仅在应用程序级别进行检查。

仅在应用程序级别检查数据在许多方面都很有吸引力。它使实现更容易,并且通常可以正常工作。在 Rails 的情况下,它还允许您使用单一语言(即 Ruby)并在您的模型文件中放置完整性检查,您将在那里使用它们。

但正如我已经说过的,如果我的检查仅在应用程序中,那么当我尝试从应用程序外部访问数据库时,我很可能会把事情搞砸,即使只是意外地。现在,我承认这是许多 Web 应用程序的设计方式,而且这并不是一个致命的缺陷。但这是我将在本文中尽量避免的事情。

尝试反其道而行之将是一个更大的错误——也就是说,仅在数据库中进行检查,而不是在应用程序级别进行检查。例如,想象一下,如果您在数据库中有一个“NOT NULL”约束,这样特定的列不能包含 NULL 值。如果您没有在应用程序层防止此类事情,您最终可能会尝试将该数据放入您的数据库。虽然数据库本身不会被损坏,但应用程序会生成错误,使用户感到困惑和沮丧。

因此,尽管这可能会令人不安和烦恼,但最好的方法可能是重复一些工作——在数据库中添加约束,然后在应用程序中也添加约束。您可以争辩说,您应该在第三个位置(即 HTML 表单)中设置约束,用户通常会使用该表单向服务器提交信息。幸运的是,至少有一种技术可以自动处理此类事情。Rails 的 client_side_validations gem 尽可能地复制验证,并将它们放入用户视图中的 JavaScript 中。

外键

有了这个背景,现在让我们假设我想实现一个简单的预约日历。为了实现我的日历,我需要跟踪人员(每个人都有姓名、姓氏、电子邮件地址和电话号码)和预约(这将指示预约的日期/时间、我与之会面的人以及关于会议本身的注释)。

在数据库级别,表将非常简单。如果我手动创建表,这就是我创建表的方式


CREATE TABLE People (
    id SERIAL,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    email TEXT NOT NULL,
    phone TEXT NOT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE Appointments (
    id SERIAL,
    person_id INTEGER REFERENCES People NOT NULL,
    notes TEXT NOT NULL,
    PRIMARY KEY(id)
);

对于本次讨论的目的而言,这里重要的是 Appointments 表定义中的 REFERENCES People 子句。有了 REFERENCES 子句,我可以执行以下操作


INSERT INTO People (first_name, last_name, email, phone)
    VALUES ('Reuven', 'Lerner', 'reuven@lerner.co.il',
    ↪'847-230-9795');
INSERT INTO Appointments (person_id, notes) VALUES (1, 'Meet with
 ↪myself');

现在,如果我尝试从 People 表中删除自己,会发生以下情况


atf=# DELETE FROM People WHERE id = 1;
ERROR:  update or delete on table "people" violates foreign key
constraint "appointments_person_id_fkey" on table "appointments"
DETAIL:  Key (id)=(1) is still referenced from table "appointments".

由于 Appointments.person_id 是 People.id 的外键,如果 Appointments 引用了 People 中的行,我就无法从 People 中删除该行。这种关系完整性对于我的应用程序来说是一件好事。无论您如何看待它,它都意味着如果人员被安排了预约,我就无法从系统中删除他们。

Rails 集成

如果这些表是我在 Ruby on Rails 中开发的 Web 应用程序的一部分,我将使用数据库迁移来创建它们。迁移看起来像这样(如果合并到一个文件中)


class CreatePeopleAndAppointments < ActiveRecord::Migration

  def change
    create_table :people do |t|
      t.string :first_name, :null => false
      t.string :last_name, :null => false
      t.string :email, :null => false
      t.string :phone, :null => false

      t.timestamps
    end

    create_table :appointments do |t|
      t.integer :person_id, :null => false
      t.text :notes, :null => false

      t.timestamps
    end
  end
end

现在,这将为我提供表定义,但不会为我提供外键,这意味着我可以通过删除单个“People”记录来损坏数据库。为了做到这一点,至少在默认的 Rails 配置中,我需要在迁移中使用“execute”命令向数据库发送显式 SQL。

幸运的是,有一种更简单的方法。Matthew Higgins 编写的 Foreigner gem,它既可以与 MySQL 一起使用,也可以与 PostgreSQL 一起使用,它添加了语法,允许您在迁移中创建和删除外键。例如,在 Foreigner 激活的情况下——将其放入 Gemfile 然后运行 bundle install——我可以添加一个新的迁移,执行以下操作


class AddForeignKey < ActiveRecord::Migration
  def up
    add_foreign_key :appointments, :people
  end

  def down
    remove_foreign_key :appointments, :people
  end
end

果然,在运行此迁移后,我得到了我希望的外键。同样,对于我来说,在我的 Rails 模型中进行此检查仍然很重要;否则,我很容易让自己陷入数据库禁止但模型允许的情况,从而为我的用户生成错误错误。

注意:如果您在已经用一些数据填充数据库后添加外键,您可能会遇到麻烦。这是因为如果一个或多个行未能遵守声明的约束,PostgreSQL 将不允许您添加外键。NOT NULL 约束还确保您指向系统中的某个人。换句话说,每个预约都必须与某人进行,并且必须与 People 表中的某人进行。

现在,假设您已经在处理一个项目,但您忽略了定义外键。您可以遍历每个表,找出哪个表指向哪个表,然后相应地处理它,添加上面显示的迁移类型。但是 Immigrant gem(也是由 Matthew Higgins 编写和发布的)会查看 Rails 模型,并为每个指向另一个模型中 belongs_to 列的 has_one 和 has_many 列添加外键。

其他检查

Foreigner 是向前迈出的一大步,有助于确保您的数据完整性。但还有一个问题,比外键问题更微妙,我仍然容易受到影响。虽然我已确保 person_id 不会为 NULL 并且将指向 People 表中的记录,但我尚未确保 People 表将包含有效且合理的值。也就是说,尽管 first_name、last_name、e-mail 和 phone 列不会包含 NULL,但它们可能包含空字符串或无效值(例如,电子邮件地址)。

在数据库级别,我可以使用 CHECK 子句处理此类问题。这样的子句确保非法数据——对于我想定义的任何“非法”值——不能放入数据库中。这可以是任何东西,从文本字符串的某个最小或最大长度,到文本中的模式,再到最小或最大数字。例如,我经常喜欢指示数据库可能不包含低于 0 的价格,并且电子邮件地址需要匹配非常基本的正则表达式。(请注意,匹配电子邮件地址并非易事,至少如果您想正确地执行它。)

因此,考虑到我的 People 表,我可以定义一组 CHECK 子句,以确保 first_name 字段为非空。换句话说,first_name 不能为 NULL,但也不能是空字符串


ALTER TABLE People ADD CONSTRAINT
    people_first_name_non_empty_chks CHECK (first_name <> '');

请注意,尽管我可以在此单个约束中添加许多检查,但我更喜欢不这样做。这使我能够更灵活地添加和删除约束,并且还确保当约束被违反时,PostgreSQL 将准确地告诉我是哪个约束。

现在,我如何在我的 Rails 迁移中实现这一点,以及我是否想要这样做?正如您可能想象的那样,我的答案是,这确实是一件好事,应该包含在数据库中。

再一次,一个 Ruby gem 提供了帮助。这个 gem,sexy_pg_constraints,是由 Maxim Chernyak 编写的,但此后它被 fork 并由其他人维护。

我可以通过将以下内容添加到我的 Gemfile 中,将其包含在我的 Rails 应用程序中


gem 'Empact-sexy_pg_constraints', :require => 'sexy_pg_constraints'

并通过取消注释 config/application.rb 中的以下行


config.active_record.schema_format = :sql

简而言之,sexy_pg_constraints 添加了许多附加属性,您可以将这些属性传递给迁移中的 add_column 或 change_column 调用。例如,假设我想确保,像以前一样,first_name 列永远不会为空——既不是 NULL 也不是空字符串。我可以这样做,通过说


class AddConstraints < ActiveRecord::Migration
  def up
    constrain :people, :first_name, :not_blank => true
  end

  def down
    deconstrain :people, :first_name
  end
end

在我应用此迁移后,我在我的表定义中发现了以下内容


"people_first_name_not_blank" CHECK (length(btrim(first_name::text)) > 0)

换句话说,我正在检查以确保在删除字符串两侧的所有空格后,长度大于 0。听起来对我来说有效!

sexy_pg_constraints 带有大量选项,包括白名单、黑名单、匹配电子邮件地址和检查数据格式。据我估计,这个 gem 唯一缺少的是一种使模型文件能够自动与数据库和/或迁移通信的方法,这样您就不必在两个地方手动添加这些内容。即便如此,通过添加这些约束,您可以在不超出 Rails 迁移框架太远的情况下提高数据的完整性。

结论

数据库约束的存在是为了将人们从他们自己手中拯救出来,并且它们是关系型数据库提供的一项很棒的功能。为了预先进行一些工作,并在运行时付出一些小的性能代价,您可以确保您的数据保持完整。我认为在数据库和应用程序级别进行这种验证都很重要。正如我在此处解释的那样,许多 Ruby gem 使在 Ruby on Rails 中进行这种集成成为可能。

资源

关于 PostgreSQL 和约束的信息在 Web 上 https://postgresql.ac.cn/docs/current/static/ddl-constraints.html,任何对此主题感兴趣的人都应该阅读它。

Foreigner 和 Immigrant gem 在 GitHub 上 https://github.com/jenseng/foreignerhttps://github.com/jenseng/immigrant,分别。

sexy_pg_constraints gem 的最新分支在 https://github.com/carbonfive/sexy_pg_constraints

数据库图片 via Shutterstock.com。

Reuven M. Lerner,一位长期从事 Web 开发的开发者,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并为程序员发布免费的每周新闻通讯,网址为 http://lerner.co.il/newsletter。Reuven 的 Twitter 账号是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论