【问题标题】:Database design: Matching sql database keys to php constants?数据库设计:将 sql 数据库键与 php 常量匹配?
【发布时间】:2011-04-01 04:34:18
【问题描述】:

嗯,这是一个简单的设计问题,我想了很多次,但从未找到令人满意的解决方案。我的例子是 php-sql,但这当然也适用于其他语言。

我有一个只包含很少条目的小型数据库表,而且几乎不需要更新。例如这个usertype 表:

usertype_id (primary key)  | name       | description
---------------------------+------------+-------------------
1                          | 'admin'    | 'Administrator'
2                          | 'reguser'  | 'Registered user'
3                          | 'guest'    | 'Guest'

现在在 php 代码中,我经常需要检查或比较我正在处理的用户类型。由于用户类型存储在数据库中,我可以:

1) 在类实例化时从用户类型表中选择 *,并将其存储在一个数组中。
然后所有的 id 都可用于代码,我可以做一个简单的选择来获取我需要的行。每次实例化类时,此解决方案都需要一个数组和一个 db 查询。

$query = "SELECT info, foo FROM user WHERE usertype_id = ".$usertypes['admin'];

2) 使用name 列选择正确的usertype_id,这样我们就可以有效地与其他表连接。这或多或少等同于 1) 但不需要在 php 对象中缓存整个 usertype 表:

$query = "SELECT info, foo FROM user JOIN usertype USING (usertype_id) WHERE usertype.name = 'admin' ";

3) 定义与用户类型表中的键匹配的常量:

// As defines
define("USERTYPE_ADMIN",1);
define("USERTYPE_REGUSER",2);

//Or as class constants
const USERTYPE_ADMIN = 1;
const USERTYPE_REGUSER = 2;

然后做一个简单的选择。

$query = "SELECT info, foo FROM user WHERE usertype_id = " . USERTYPE_ADMIN;

这可能是最节省资源的解决方案,但维护起来很糟糕,因为如果您需要修改 usertype 表中的某些内容,则必须同时更新表和代码。..

4) 废弃usertype 表,只保留php 代码中的类型。我真的不喜欢这样,因为它允许任何值进入数据库并分配给用户类型。但也许,考虑到所有的事情,它并没有那么糟糕,我只是把应该简单的事情复杂化了..

无论如何,总而言之,我最喜欢的解决方案是#2,因为它是连贯的并且在usertype.name 上有一个索引,它不会那么糟糕。但我经常使用的是#3,以提高效率。

你会怎么做?有更好的解决方案吗?

(编辑:#2 中的固定查询)

【问题讨论】:

    标签: php design-patterns database-design


    【解决方案1】:

    我几乎总是选择选项 3)。您可以根据数据库中可用的内容自动生成所需的代码。您唯一需要记住的是,当您添加另一个角色时,您必须运行脚本来更新/重写该信息(但如果您使用 phing 或类似的构建工具来部署您的应用程序,只需添加一个构建规则将其添加到您的部署脚本中,并且在您部署代码时始终运行它:p)。

    【讨论】:

      【解决方案2】:

      我建议#3避免无用的查询,并防止意外修改现有数据库表行时行为更改的风险:

      • 在模型类中添加必要的常量:

        class Role // + use namespaces if possible
        {
          // A good ORM could be able to generate it (see @wimvds answer)
          const ADMIN = 1;
          const USER = 2;
          const GUEST = 3;
        
          //...
        

        }

      • 那么这样查询就有意义了:

        $query = "SELECT info, foo FROM user WHERE role_id = ".Role::ADMIN;
        

        使用 ORM(例如下面示例中的 Propel),您最终会这样做:

        $isAdminResults = UserQuery::create()->filterByRoleId(Role::ADMIN);
        

      【讨论】:

        【解决方案3】:

        为什么不对 DB 表进行非规范化处理,而不是使用 usertype_id,而是使用字符串类型 (admin) 的 usertype。然后在 PHP 中你可以做define('USERTYPE_ADMIN', 'admin');。如果您想添加用户类型,它可以让您不必修改两个地方...

        如果你真的担心任何值进入,你总是可以将列设置为 ENUM 数据类型,这样它就可以自我管理......

        【讨论】:

        • 如果类型表将只被一个表使用(如示例中所示),这将起作用。但是如果多个表需要使用相同的枚举类型呢?
        • 好吧,那么您将需要使用规范化方法。然后你要么不断地在数据库中查询值,要么陷入困境。要么这样,要么在两个地方维护它(这不好玩)......
        【解决方案4】:

        对于将包含“类型”值的表,尤其是当预计此类表会随时间变化时,我倾向于使用简单的方法: 添加带有唯一键的名为 hid 的 Varchar 列(来自“人类可读的 id”)。然后我用对人类有意义的 id 填充它:

        usertype_id (primary key)  | name       | description       | hid (unique key)
        ---------------------------+------------+-------------------+---------------
        1                          | 'admin'    | 'Administrator'   | 'admin'
        2                          | 'reguser'  | 'Registered user' | 'user'
        3                          | 'guest'    | 'Guest'           | 'guest'
        

        当您需要实际 id 时,您必须根据隐藏列进行选择,即

        select usertype_id from tablename where hid = "admin"
        

        这不是一种有效的方法,但可以确保您的应用程序在不同部署之间的兼容性(即一个客户端可能有 1.admin、2.guest;其他客户端可能有 1.admin、2.user 等)。对于您的情况,我认为#3 非常合适,但如果您希望拥有超过 10 个不同的用户角色 - 请尝试“隐藏”方法。

        【讨论】:

        • 这就是我所说的解决方案#2。虽然我不得不承认我为人类可读的 id 列 (name) 选择的名称不是很直观。
        • 哦,我的错。对 varchar 列上的连接感到困惑(我认为效率不高)。顺便说一句,这个 sql 语句会产生任何结果吗?!关于这种方法的另一件事:最好限制它可以包含的值,例如: ereg("^[az]{1}[a-z0-9_]{0,254}$", $hid) 即允许小写、字母数字、下划线。如果您不是唯一可以输入值的人,这很有用...
        • 是的,名称列应该是唯一索引,否则效率会很低。你是对的,#2中的查询是错误的:S - 刚刚修复它,谢谢!
        【解决方案5】:

        您在这里使用任何类型的框架吗?这些值是否可以存储在单个源中 - 一个配置文件 - 它既可以在 PHP 中创建对象列表,也可以在引导数据库时填充表?我是从 Rails 的角度考虑的,因为我已经有一段时间没有编写任何 PHP 了。解决方案可能会有固定装置。

        【讨论】:

          【解决方案6】:

          为什么不让它只是

          foreach (getdbarr("SELECT * FROM usertype") as $row)  {
            define($row['name'],$row['id']);
          }
          

          【讨论】:

            【解决方案7】:

            您不需要在每个查询中都使用 JOIN 来获取有关类型/角色的信息。您可以在数据访问对象 (DAO) 中将“用户”模型和“角色”模型分开,尤其是因为用户类型的记录很少。

            在大多数情况下,如果我的选项数量有限,否则我会加入到一个大表中,我会将它们作为关联数组缓存在 memcached 中。如果我需要有关特定关系(如角色)的一些信息,我只是懒加载它。

            $user = DAO_User::get(1); // this pulls a JOIN-less record
            $role = $user->getRole(); // lazy-load
            

            $user->getRole() 的代码可以是这样的:

            public function getRole() { 
              // This comes from a cache that may be called multiple 
              // times per request with no penalty (i.e. store in a registry)
              $roles = DAO_UserRoles::getAll();
            
              if(isset($roles[$this->role_id]))
                return $roles[$this->role_id];
            
              return null; // or: new Model_UserRole();
            }
            

            如果您想显示一个包含 1000 个用户的列表,这也适用。您可以简单地从单个 $roles 关联数组中呈现该列的值。

            这是 SQL 端的一项重大性能改进,它对降低代码库的复杂性大有帮助。如果您在用户表上有几个其他外键,您仍然可以在需要时使用这种方法来获取必要的信息。这也意味着您可以拥有可靠的 Model_* 类,而不必为您可能加入的每个可能的表组合创建混合 - 这比简单地获取结果集、迭代并释放它要好得多。

            即使您的 JOIN 两边都有超过 100 行,您仍然可以使用延迟加载方法来处理不频繁或高度冗余的信息。在您的代码中使用合理的缓存服务,多次调用 DAO_UserRole::get(1500) 不会受到任何惩罚,因为在同一请求期间的后续调用不应两次访问数据库。在大多数情况下,每页仅显示 10-25 行(每页 1000 行),而延迟加载将使您的数据库引擎不必在实际需要之前加入所有无关的行。

            执行 JOIN 的主要原因是您的 WHERE 逻辑是否需要它,或者您需要从外键中按数据排序。将 JOIN 视为过于昂贵是一个好习惯。

            【讨论】:

              【解决方案8】:

              对于基本静态的查找表,我通常制作静态常量文件(例如您的#3)。我一般使用的类如:

              namespace Constants;
              class UserTypes {
                  const ADMIN = 1;
                  const USER = 2;
                  const GUEST = 3;
              }
              
              $id = Constants\UserTypes::ADMIN;
              

              当我使用多变的查找镜头时,我会将其拉入一个对象,然后将其缓存 24 小时。这样,它每天只更新一次。这将使您免于进行数据库往返,但让您可以轻松地处理代码中的事情。

              【讨论】:

                【解决方案9】:

                是的,您对避免#3 并坚持#2 是正确的。尽可能地,当您使用用户类型表包含角色然后使用 id 值将它们与用户表相关联时的查找应该保留在数据库中。如果您使用常量,那么数据必须始终依赖于您的 php 代码来进行解释。此外,您可以通过使用外键(在服务器允许的情况下)来强制数据完整性,它将允许您将报告从您的 php 代码移植到其他报告工具。维护也变得更容易。如果您使用#3,数据库管理员将不需要了解 php 来推导数字的含义,如果他们被要求帮助报告开发。它可能看起来不太相关,但在维护方面,使用存储过程而不是在您的 php 代码中嵌入 sql 在几个方面也对维护友好,并且对 DBA 也有利。

                【讨论】:

                  【解决方案10】:

                  我会选择选项 #2 并使用预期使用的连接。你永远不知道未来会发生什么,今天做好准备总是更好!

                  关于尽可能让数据库单独进行此类操作,长期缓存也是有可能的。对于这条路线,在 PHP 中一个选项是使用文件缓存,它只会在时间需要时更新。对于我创建的框架,这是一个示例;我很想知道人们的想法:

                  注意:

                  (LStore, LFetch, GetFileName) 属于静态调用的缓存对象。

                  (Blobify 和 Unblobify) 属于一个始终处于活动状态的 SystemComponent 对象

                  每条缓存数据都有一个键。这是你唯一需要记住的事情

                  public function LStore($key,$data, $blnBlobify=true) {
                      /* Opening the file in read/write mode */
                      $h = fopen(self::GetFileName($key, 'longstore'),'a+');
                      if (!$h) throw new Exception('Could not write to cache');
                      flock($h,LOCK_EX); // exclusive lock, will get released when the file is closed
                      fseek($h,0); // go to the start of the file
                      /* truncate the file */
                      ftruncate($h,0);
                      if($blnBlobify==true) { $data = SystemComponent::Blobify(array($data)); }
                      If (fwrite($h,$data)===false) {
                          throw new Exception('Could not write to cache');
                      }
                      fclose($h);
                  }   
                  public function LFetch($key) {
                      $filename = self::GetFileName($key, 'longstore');
                      if (!file_exists($filename)){ return false;}
                      $h = fopen($filename,'r');
                      if (!$h){ return false;}
                      /* Getting a shared lock */
                      flock($h,LOCK_SH);
                      $data = file_get_contents($filename);
                      fclose($h);
                      $data = SystemComponent::Unblobify($data);
                      if (!$data) {
                          /* If unserializing somehow didn't work out, we'll delete the file */
                          unlink($filename);
                          return false;
                      }
                      return $data;
                  }
                  /* This function is necessary as the framework scales different directories */
                  
                  private function GetFileName($key, $strCacheDirectory='') {
                      if(!empty($strCacheDirectory)){ 
                          return SystemComponent::GetCacheAdd() . $strCacheDirectory.'/' .  md5($key); 
                      } else {    
                          return SystemComponent::GetCacheAdd() . md5($key);
                      }
                  }
                  public function Blobify($Source){
                      if(is_array($Source)) { $Source = serialize($Source); }
                  
                      $strSerialized = base64_encode($Source);
                  
                      return $strSerialized;
                  
                  }
                  public function Unblobify($strSerialized){
                      $Decoded = base64_decode($strSerialized);
                      if(self::CheckSerialized($Decoded)) { $Decoded = unserialize($Decoded); }
                  
                      return $Decoded;    
                  
                  }
                  function CheckSerialized($Source){
                      $Data = @unserialize($Source);
                      if ($Source === 'b:0;' || $Data !== false) {
                          return true;
                      } else {
                          return false;
                      }
                  }  
                  

                  现在在访问实际数据时,我只调用 fetch。为了确保它是最新的,我告诉它存储。在您的情况下,这将是在更新用户类型表之后。

                  【讨论】:

                  • 我知道这对于您的需要来说似乎有点过头了,但它可以在整个网站上一次又一次地使用。
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-11-04
                  • 2021-03-30
                  • 2011-04-14
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多