平面文件数据库

132

在PHP中创建平面文件数据库结构的最佳实践是什么?

有很多比较成熟的PHP平面文件框架试图实现类似SQL查询语法,但对于我来说,在大多数情况下这是过度的(此时我会选择使用数据库)。

是否有任何优雅的技巧可以在代码开销很小的情况下获得良好的性能和功能?


1
我想补充一下,这里有一个关于平面文件数据库(Flat File Database)的包:https://github.com/tmarois/Filebase。我知道这是一个老问题,但这个包是最新的版本并且得到了维护,而且包含了许多被忽略的功能 - tmarois
我正在开发一个CMS,并使用平面文本文件文本数据库。它花费了很多时间来制作和重构,但它完美地工作。使用完全索引和优化的数据库将会更快地执行查询。然而,我通过存储元数据并进行仔细的组织和结构来避免查询的需要。当我需要数据时,我获取它而不需要使用for loop(除非我在使用文件夹中的所有数据),因此它比使用数据库要快得多。我可以详细说明并给出一个非常好的答案,但不幸的是这个问题已经关闭了。 - Dan Bray
11个回答

81

那么,扁平数据库的本质是什么?它们是大还是小?它们是简单的数组,其中包含数组吗?如果它是一些简单的东西,比如像用户配置文件这样构建的:

$user = array("name" => "bob", 
              "age" => 20,
              "websites" => array("example.com","bob.example.com","bob2.example.com"),
              "and_one" => "more");

并且保存或更新该用户的 数据库记录

$dir = "../userdata/";  //make sure to put it bellow what the server can reach.
file_put_contents($dir.$user['name'],serialize($user));

加载用户的记录

function &get_user($name){
    return unserialize(file_get_contents("../userdata/".$name));
}

但是,这个实现方式将再次根据您需要的数据库的应用程序和性质而有所不同。


51
你可以考虑使用SQLite。它几乎和平面文件一样简单,但你可以使用SQL引擎进行查询。它也很适合与PHP一起使用。

6
SQLite在默认情况下内置于PHP 5.0+,但从PHP 5.4+版本开始被取消支持!截至我写下这篇回答的时间(2012年7月),最新系统默认情况下将无法使用SQLite。官方声明请参见此处:http://www.php.net/manual/en/sqlite.requirements.php。 - Sliq
如果您拥有服务器访问权限,安装SQLite PDO驱动程序非常简单。在运行Apache2的Ubuntu/Debian上,只需执行以下操作:apt-get install php5-sqlite service apache2 restart - siliconrockstar
5
回应 @Sliq 的评论说“SQLite已经停止使用”有一定的道理:名为“SQLite”的扩展已经被停用,现在默认启用“SQLite3”。http://php.net/manual/zh/sqlite.installation.php
"自从 PHP 5.0 版本开始,此扩展就随 PHP 捆绑发布了。自 PHP 5.4 开始,这个扩展只能通过 PECL 获得。" http://php.net/manual/zh/sqlite3.installation.php
"从 PHP 5.3.0 版本起,默认启用 SQLite3 扩展。""该扩展曾短暂作为 PECL 扩展,但那个版本仅建议用于实验性用途。"
- pjvleeuwen
你没有回答这个问题。 - JG Estiot

23
在我看来,以你所指的方式(和你接受的答案)使用“平面文件数据库”并不一定是最佳选择。首先,如果有人进入并编辑文件,使用serialize()和unserialize()可能会导致严重的问题(实际上,他们可以在您的“数据库”中放置任意代码以在每次运行时运行)。
个人而言,我会说-为什么不展望未来呢?已经有很多次,因为我一直在创建自己的“专有”文件,而项目已经扩展到需要数据库的地步,我在想“你知道吗,我希望一开始就为数据库编写这个”。 因为代码的重构需要太多的时间和精力。
从这个经验中,我学到了使我的应用程序具备未来性,这样当它变得更大时,我就不必去花费数天的时间进行重构。我该怎么做?
SQLite。它作为一个数据库工作,使用SQL,并且非常容易改变为MySQL(特别是如果您像我一样使用抽象类来操纵数据库!)
事实上,特别是对于“接受答案”的方法,它可以大幅减少应用程序的内存使用(您不必将所有“RECORDS”加载到PHP中)。

没错。serialize() 也可以非常有用。我认为设计一个可行的系统的诀窍是找到一种索引数据节点的方法,而不会让自己陷入复杂性的困境。 - saint_groceon
我给你一个场景,你不想使用SQLite或实际上任何数据库,而是直接使用文件系统。你的系统中有8000万个交易记录,每个交易记录的长度只有126个字符,你每秒钟向其中添加1800个交易,并且只在午夜后一次读取这些数据。 - AaA
你有使用示例吗? - felwithe

13
一个我正在考虑的框架是用于博客平台。由于几乎任何你想要的数据视图都会按日期排序,所以我在考虑以下结构:
每个内容节点一个目录:
./content/YYYYMMDDHHMMSS/

每个节点的子目录,包括
/tags  
/authors  
/comments  

除了节点目录中的简单文本文件,还可以预渲染内容等等。
这将允许使用简单的 PHP glob() 调用(并可能对结果数组进行反转)来查询内容结构中的几乎任何内容:
glob("content/*/tags/funny");  

返回包括所有标记为“有趣”的文章的路径。

10
以下是我们在 Lilina 中使用的代码:
<?php
/**
 * Handler for persistent data files
 *
 * @author Ryan McCue <cubegames@gmail.com>
 * @package Lilina
 * @version 1.0
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */

/**
 * Handler for persistent data files
 *
 * @package Lilina
 */
class DataHandler {
    /**
     * Directory to store data.
     *
     * @since 1.0
     *
     * @var string
     */
    protected $directory;

    /**
     * Constructor, duh.
     *
     * @since 1.0
     * @uses $directory Holds the data directory, which the constructor sets.
     *
     * @param string $directory 
     */
    public function __construct($directory = null) {
        if ($directory === null)
            $directory = get_data_dir();

        if (substr($directory, -1) != '/')
            $directory .= '/';

        $this->directory = (string) $directory;
    }

    /**
     * Prepares filename and content for saving
     *
     * @since 1.0
     * @uses $directory
     * @uses put()
     *
     * @param string $filename Filename to save to
     * @param string $content Content to save to cache
     */
    public function save($filename, $content) {
        $file = $this->directory . $filename;

        if(!$this->put($file, $content)) {
            trigger_error(get_class($this) . " error: Couldn't write to $file", E_USER_WARNING);
            return false;
        }

        return true;
    }

    /**
     * Saves data to file
     *
     * @since 1.0
     * @uses $directory
     *
     * @param string $file Filename to save to
     * @param string $data Data to save into $file
     */
    protected function put($file, $data, $mode = false) {
        if(file_exists($file) && file_get_contents($file) === $data) {
            touch($file);
            return true;
        }

        if(!$fp = @fopen($file, 'wb')) {
            return false;
        }

        fwrite($fp, $data);
        fclose($fp);

        $this->chmod($file, $mode);
        return true;

    }

    /**
     * Change the file permissions
     *
     * @since 1.0
     *
     * @param string $file Absolute path to file
     * @param integer $mode Octal mode
     */
    protected function chmod($file, $mode = false){
        if(!$mode)
            $mode = 0644;
        return @chmod($file, $mode);
    }

    /**
     * Returns the content of the cached file if it is still valid
     *
     * @since 1.0
     * @uses $directory
     * @uses check() Check if cache file is still valid
     *
     * @param string $id Unique ID for content type, used to distinguish between different caches
     * @return null|string Content of the cached file if valid, otherwise null
     */
    public function load($filename) {
        return $this->get($this->directory . $filename);
    }

    /**
     * Returns the content of the file
     *
     * @since 1.0
     * @uses $directory
     * @uses check() Check if file is valid
     *
     * @param string $id Filename to load data from
     * @return bool|string Content of the file if valid, otherwise null
     */
    protected function get($filename) {
        if(!$this->check($filename))
            return null;

        return file_get_contents($filename);
    }

    /**
     * Check a file for validity
     *
     * Basically just a fancy alias for file_exists(), made primarily to be
     * overriden.
     *
     * @since 1.0
     * @uses $directory
     *
     * @param string $id Unique ID for content type, used to distinguish between different caches
     * @return bool False if the cache doesn't exist or is invalid, otherwise true
     */
    protected function check($filename){
        return file_exists($filename);
    }

    /**
     * Delete a file
     *
     * @param string $filename Unique ID
     */
    public function delete($filename) {
        return unlink($this->directory . $filename);
    }
}

?>

它将每个条目存储为单独的文件,我们发现这是足够高效的(没有加载不必要的数据,并且保存速度更快)。


9

在我看来,如果你想避免自己编写代码的话,有以下两个...啊,三个选择:

  1. SQLite

如果你熟悉PDO,那么你可以安装一个支持SQLite的PDO驱动。虽然我并没有用过,但我曾经使用PDO与MySQL工作多年。现在我正准备在当前项目中尝试一下这种方法。

  1. XML

对于相对较少的数据,我已经做了很多次这种方法。 XMLReader 是一个轻量级、向前读取、游标式的类。而SimpleXML 可以将XML文档读入一个对象中,你可以像访问其他类实例一样访问它。

  1. JSON (更新)

如果需要处理较小的数据,可以采用此选项,只需读/写文件和json_decode/json_encode即可。不确定PHP是否提供了一种结构来遍历JSON树而无需将其全部加载到内存中。


好的想法。为什么不用JSON呢? - Machado
1
因为当我写这篇文章时,JSON还不是一个很流行的东西,哈哈。 - siliconrockstar
哇哈哈,抱歉我之前没看到帖子的日期。那很好,如果有人想增加答案,JSON仍然是一个可选项。 - Machado

8
如果你要使用平面文件来持久化数据,建议使用XML来结构化数据。PHP内置了一个XML解析器

请遵循 XML 的可读性规则,否则您可以使用序列化、JSON 或其他格式。 - Ben
2
非常糟糕的建议。XML 永远不应该被使用。它是一个臃肿的畸形。 - JG Estiot
@JGEstiot 能否进一步解释一下? - UncaughtTypeError

7
如果您想获得易于阅读的结果,您也可以使用这种类型的文件:
ofaurax|27|male|something|
another|24|unknown||
...

这样做的好处是只有一个文件,易于调试(并可以手动修复),以后可以添加字段(在每行末尾),而且PHP代码简单(对于每行,按|拆分)。
然而,缺点是如果要搜索某些内容,您需要解析整个文件(如果有数百万条记录,则不太好),并且您需要处理数据中的分隔符(例如,如果昵称是WaR|ordz)。

7

这个项目是一个实用的解决方案,可以激发灵感:
https://github.com/mhgolkar/FlatFire
它使用多种策略来处理数据...
[摘自Readme文件]

自由或结构化或混合

- STRUCTURED
Regular (table, row, column) format.
[DATABASE]
/   \
TX  TableY
    \_____________________________
    |ROW_0 Colum_0 Colum_1 Colum_2|
    |ROW_1 Colum_0 Colum_1 Colum_2|
    |_____________________________|
- FREE
More creative data storing. You can store data in any structure you want for each (free) element, its similar to storing an array with a unique "Id".
[DATABASE]
/   \
EX  ElementY (ID)
    \________________
    |Field_0 Value_0 |
    |Field_1 Value_1 |
    |Field_2 Value_2 |
    |________________|
recall [ID]: get_free("ElementY") --> array([Field_0]=>Value_0,[Field_1]=>Value_1...
- MIXD (Mixed)
Mixed databases can store both free elements and tables.If you add a table to a free db or a free element to a structured db, flat fire will automatically convert FREE or SRCT to MIXD database.
[DATABASE]
/   \
EX  TY

7

我编写了两个简单的函数,旨在将数据存储到文件中。您可以自行判断在这种情况下是否有用。 重点是将php变量(无论是数组、字符串还是对象)保存到文件中。

<?php
function varname(&$var) {
    $oldvalue=$var;
    $var='AAAAB3NzaC1yc2EAAAABIwAAAQEAqytmUAQKMOj24lAjqKJC2Gyqhbhb+DmB9eDDb8+QcFI+QOySUpYDn884rgKB6EAtoFyOZVMA6HlNj0VxMKAGE+sLTJ40rLTcieGRCeHJ/TI37e66OrjxgB+7tngKdvoG5EF9hnoGc4eTMpVUDdpAK3ykqR1FIclgk0whV7cEn/6K4697zgwwb5R2yva/zuTX+xKRqcZvyaF3Ur0Q8T+gvrAX8ktmpE18MjnA5JuGuZFZGFzQbvzCVdN52nu8i003GEFmzp0Ny57pWClKkAy3Q5P5AR2BCUwk8V0iEX3iu7J+b9pv4LRZBQkDujaAtSiAaeG2cjfzL9xIgWPf+J05IQ==';
    foreach($GLOBALS as $var_name => $value) {
        if ($value === 'AAAAB3NzaC1yc2EAAAABIwAAAQEAqytmUAQKMOj24lAjqKJC2Gyqhbhb+DmB9eDDb8+QcFI+QOySUpYDn884rgKB6EAtoFyOZVMA6HlNj0VxMKAGE+sLTJ40rLTcieGRCeHJ/TI37e66OrjxgB+7tngKdvoG5EF9hnoGc4eTMpVUDdpAK3ykqR1FIclgk0whV7cEn/6K4697zgwwb5R2yva/zuTX+xKRqcZvyaF3Ur0Q8T+gvrAX8ktmpE18MjnA5JuGuZFZGFzQbvzCVdN52nu8i003GEFmzp0Ny57pWClKkAy3Q5P5AR2BCUwk8V0iEX3iu7J+b9pv4LRZBQkDujaAtSiAaeG2cjfzL9xIgWPf+J05IQ==')
        {
            $var=$oldvalue;
            return $var_name;
        }
    }
    $var=$oldvalue;
    return false;
}

function putphp(&$var, $file=false)
    {
    $varname=varname($var);
    if(!$file)
    {
        $file=$varname.'.php';
    }
    $pathinfo=pathinfo($file);
    if(file_exists($file))
    {
        if(is_dir($file))
        {
            $file=$pathinfo['dirname'].'/'.$pathinfo['basename'].'/'.$varname.'.php';
        }
    }
    file_put_contents($file,'<?php'."\n\$".$varname.'='.var_export($var, true).";\n");
    return true;
}

我发现这很有趣,而且这是更好的方法,因为我们只需将格式化的数组转储到文件中。我们不需要再构建它,只需读取即可。此外,编辑变量也更加容易。虽然我不会用它来存储大量数据,但我发现它可以实用地存储程序模块而无需使用数据库。谢谢。 - m3nda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接