15

目前我的代码(PHP)中有太多的 SQL 查询。例如...

// not a real example, but you get the idea...
$results = $db->GetResults("SELECT * FROM sometable WHERE iUser=$userid");
if ($results) {
    // Do something
}

我正在研究使用存储过程来减少这种情况并使事情变得更加健壮,但我有一些担忧..

我在网站上使用了数百个不同的查询,其中许多非常相似。当所有这些查询从其上下文(使用结果的代码)中删除并放置在数据库的存储过程中时,我应该如何管理它们?

4

10 回答 10

30

最适合您的行动方案将取决于您如何处理数据访问。您可以采取三种方法:

  • 使用存储过程
  • 将查询保留在代码中(但将所有查询放入函数并修复所有内容以使用 PDO 作为参数,如前所述)
  • 使用 ORM 工具

如果您想将您自己的原始 SQL 传递给数据库引擎,那么如果您想要做的只是从 PHP 代码中获取原始 SQL,但保持它相对不变,那么存储过程将是您的最佳选择。存储过程与原始 SQL 的争论有点像一场圣战,但 K. Scott Allen 在一篇关于版本控制数据库的文章中提出了一个很好的观点——尽管是一次性的观点:

其次,存储过程在我眼中已经失宠。我来自 WinDNA 的灌输学校,认为应该一直使用存储过程。今天,我将存储过程视为数据库的 API 层。如果您需要数据库级别的 API 层,这很好,但我看到许多应用程序会产生创建和维护它们不需要的额外 API 层的开销。在这些应用程序中,存储过程更多的是负担而不是好处。

我倾向于不使用存储过程。我曾参与过数据库通过存储过程公开 API 的项目,但存储过程可能会施加一些自身的限制,并且这些项目都不同程度上使用代码中动态生成的原始 SQL 来访问数据库。

在 DB 上拥有 API 层可以更好地划分 DB 团队和 Dev 团队之间的职责,但代价是如果查询保留在代码中,您将拥有一些灵活性,但是 PHP 项目不太可能有相当大的规模足够多的团队从这种划分中受益。

从概念上讲,您可能应该对数据库进行版本控制。然而,实际上,您更有可能只对代码进行版本控制,而不是对数据库进行版本控制。当您更改代码时,您可能会更改查询,但如果您更改存储在数据库中的存储过程中的查询,那么当您签入代码时您可能不会签入这些查询并且您会丢失对应用程序的重要领域进行版本控制的许多好处。

不管你是否选择不使用存储过程,你至少应该确保每个数据库操作都存储在一个独立的函数中,而不是嵌入到每个页面的脚本中——本质上是你的数据库的 API 层,它使用您的代码进行维护和版本控制。如果您使用的是存储过程,这实际上意味着您的 DB 有两个 API 层,一个带有代码,一个带有 DB,如果您的项目没有单独的团队,您可能会觉得这会使事情变得不必要地复杂化。我当然愿意。

如果问题是代码整洁,有一些方法可以使代码中的 SQL 卡在其中更美观,下面显示的 UserManager 类是一个很好的开始方式 - 该类只包含与“用户”表相关的查询,每个查询在类中都有自己的方法,查询缩进到准备语句中,并像在存储过程中格式化它们一样格式化。

// UserManager.php:

class UserManager
{
    function getUsers()
    {
        $pdo = new PDO(...);
        $stmt = $pdo->prepare('
            SELECT       u.userId as id,
                         u.userName,
                         g.groupId,
                         g.groupName
            FROM         user u
            INNER JOIN   group g
            ON           u.groupId = g.groupId
            ORDER BY     u.userName, g.groupName
        ');
        // iterate over result and prepare return value
    }

    function getUser($id) {
        // db code here
    }
}

// index.php:
require_once("UserManager.php");
$um = new UserManager;
$users = $um->getUsers();
foreach ($users as $user) echo $user['name'];

但是,如果您的查询非常相似,但您的查询条件中有大量排列,例如复杂的分页、排序、过滤等,那么对象/关系映射器工具可能是您的最佳选择,尽管对现有代码进行大修的过程使用该工具可能非常复杂。

如果你决定研究 ORM 工具,你应该看看PropelYii的 ActiveRecord 组件,或者是王爷 PHP ORM,Doctrine。每一个都使您能够以编程方式使用各种复杂的逻辑构建对数据库的查询。Doctrine 是功能最齐全的,它允许您使用诸如开箱即用的嵌套集树模式之类的东西来模板化您的数据库。

在性能方面,存储过程是最快的,但通常不会超过原始 sql。ORM 工具可以通过多种方式对性能产生重大影响——低效或冗余查询、在每个请求上加载 ORM 库时的巨大文件 IO、每个查询的动态 SQL 生成……所有这些都会产生影响,但是与使用手动查询创建自己的 DB 层相比,使用 ORM 工具可以用更少的代码显着增加可用的功能。

不过, Gary Richardson是绝对正确的,如果您要继续在代码中使用 SQL,那么无论您使用的是查询还是存储过程,都应该始终使用 PDO 的准备好的语句来处理参数。PDO 会为您执行输入的清理工作。

// optional
$attrs = array(PDO::ATTR_PERSISTENT => true);

// create the PDO object
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass", $attrs);

// also optional, but it makes PDO raise exceptions instead of 
// PHP errors which are far more useful for debugging
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $pdo->prepare('INSERT INTO venue(venueName, regionId) VALUES(:venueName, :regionId)');
$stmt->bindValue(":venueName", "test");
$stmt->bindValue(":regionId", 1);

$stmt->execute();

$lastInsertId = $pdo->lastInsertId();
var_dump($lastInsertId);

警告:假设 ID 为 1,上述脚本将输出string(1) "1". PDO->lastInsertId()无论实际列是否为整数,都将 ID 作为字符串返回。这对您来说可能永远不会成为问题,因为 PHP 会自动将字符串转换为整数。

以下将输出bool(true)

// regular equality test
var_dump($lastInsertId == 1); 

但是如果您的代码期望值是整数,例如is_int或 PHP 的“真的,真的,100% 等于”运算符:

var_dump(is_int($lastInsertId));
var_dump($lastInsertId === 1);

你可能会遇到一些问题。

编辑:关于存储过程的一些很好的讨论here

于 2008-09-18T02:23:58.567 回答
4

首先,您应该在查询中使用占位符,而不是直接插入变量。PDO/MySQLi 允许您编写查询,例如:

SELECT * FROM sometable WHERE iUser = ?

API 将安全地将值替换到查询中。

我也更喜欢将我的查询放在代码中而不是数据库中。当查询与您的代码一起使用时,使用 RCS 会容易得多。

在使用 ORM 时,我有一条经验法则:如果我一次使用一个实体,我将使用该界面。如果我要报告/处理汇总的记录,我通常会编写 SQL 查询来执行此操作。这意味着我的代码中的查询很少。

于 2008-09-01T15:19:37.707 回答
3

我必须清理一个项目,其中包含许多(重复/类似)查询,其中充斥着注入漏洞。我采取的第一步是使用占位符并使用创建查询的对象/方法和源代码行标记每个查询。(将 PHP 常量METHODLINE插入 SQL 注释行)

它看起来像这样:

-- @Line:151 用户类::getuser():

SELECT * FROM USERS;

短时间内记录所有查询为我提供了一些合并查询的起点。(在哪里!)

于 2008-09-17T21:14:54.393 回答
3

我会将所有 SQL 移至单独的 Perl 模块 (.pm) 许多查询可以重用相同的函数,但参数略有不同。

开发人员的一个常见错误是深入研究 ORM 库、参数化查询和存储过程。然后我们连续几个月工作以使代码“更好”,但它只是在开发方面“更好”。您没有制作任何新功能!

仅在代码中使用复杂性来满足客户需求。

于 2009-01-10T09:55:46.080 回答
2

使用 ORM 包,任何半体面的包都可以让你

  1. 获取简单的结果集
  2. 让复杂的 SQL 接近数据模型

如果您有非常复杂的 SQL,那么视图也可以很好地使其更适合应用程序的不同层。

于 2008-09-01T11:41:16.080 回答
2

我们曾一度陷入类似的困境。我们以多种方式查询了一个特定的表,超过 50 多种。

我们最终做的是创建一个包含 WhereClause 参数值的单个 Fetch 存储过程。WhereClause 是在 Provider 对象中构造的,我们采用了 Facade 设计模式,我们可以在其中清理它以防任何SQL 注入攻击。

因此,就维护而言,它很容易修改。SQL Server 也很熟,缓存了动态查询的执行计划,所以整体性能还是不错的。

您必须根据自己的系统和需求来确定性能缺陷,但总而言之,这对我们来说非常有效

于 2008-09-01T11:48:01.693 回答
1

有一些库(例如 PEAR 中的 MDB2)使查询更容易和更安全。

不幸的是,它们的设置可能有点罗嗦,有时您必须将相同的信息传递给它们两次。我在几个项目中使用过 MDB2,我倾向于围绕它编写一层薄薄的外衣,尤其是在指定字段类型时。我通常会创建一个知道特定表及其列的对象,然后在调用 MDB2 查询函数时创建一个帮助函数来为我填充字段类型。

例如:

function MakeTableTypes($TableName, $FieldNames)
{
    $Types = array();

    foreach ($FieldNames as $FieldName => $FieldValue)
    {
        $Types[] = $this->Tables[$TableName]['schema'][$FieldName]['type'];
    }

    return $Types;
}

显然,这个对象有一个它知道的表名 -> 模式的映射,并且只提取您指定的字段的类型,并返回一个适合与 MDB2 查询一起使用的匹配类型数组。

MDB2(和类似的库)然后为您处理参数替换,因此对于更新/插入查询,您只需构建从列名到值的哈希/映射,并使用“自动执行”函数来构建和执行相关查询。

例如:

function UpdateArticle($Article)
{
    $Types = $this->MakeTableTypes($table_name, $Article);

    $res = $this->MDB2->extended->autoExecute($table_name,
        $Article,
        MDB2_AUTOQUERY_UPDATE,
        'id = '.$this->MDB2->quote($Article['id'], 'integer'),
        $Types);
}

MDB2 将构建查询,正确转义所有内容,等等。

不过,我建议使用 MDB2 来衡量性能,因为它会引入相当多的代码,如果您没有运行 PHP 加速器,这些代码可能会给您带来问题。

正如我所说,设置开销起初似乎令人生畏,但一旦完成,查询就可以更简单/更具符号性来编写和(尤其是)修改。我认为 MDB2 应该更多地了解您的架构,这将简化一些常用的 API 调用,但是您可以通过自己封装架构来减少这种烦恼,正如我上面提到的,并提供简单的访问器函数来生成数组 MDB2 需要执行这些查询。

当然,如果需要,您可以使用 query() 函数将平面 SQL 查询作为字符串执行,因此您不会被迫切换到完整的“MDB2 方式”——您可以零碎尝试,看看您是否讨厌与否。

于 2008-09-01T17:14:44.360 回答
0

这个其他问题也有一些有用的链接......

于 2008-09-01T12:08:03.753 回答
0

我尝试使用相当通用的函数并传递它们之间的差异。这样,您只有一个函数来处理大多数数据库 SELECT。显然,您可以创建另一个函数来处理所有 INSERTS。

例如。

function getFromDB($table, $wherefield=null, $whereval=null, $orderby=null) {
    if($wherefield != null) { 
        $q = "SELECT * FROM $table WHERE $wherefield = '$whereval'"; 
    } else { 
        $q = "SELECT * FROM $table";
    }
    if($orderby != null) { 
        $q .= " ORDER BY ".$orderby; 
    }

    $result = mysql_query($q)) or die("ERROR: ".mysql_error());
    while($row = mysql_fetch_assoc($result)) {
        $records[] = $row;
    }
    return $records;
}

这只是我的想法,但你明白了。要使用它,只需向函数传递必要的参数:

例如。

$blogposts = getFromDB('myblog', 'author', 'Lewis', 'date DESC');

在这种情况下, $blogposts将是一个数组数组,代表表的每一行。然后您可以只使用 foreach 或直接引用数组:

echo $blogposts[0]['title'];
于 2008-09-17T22:55:30.690 回答
0

使用像 QCodo 这样的 ORM 框架 - 您可以轻松映射现有数据库

于 2008-11-14T22:31:47.210 回答