数据库完整性和 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/foreigner 和 https://github.com/jenseng/immigrant,分别。
sexy_pg_constraints gem 的最新分支在 https://github.com/carbonfive/sexy_pg_constraints。
数据库图片 via Shutterstock.com。