Загрузка файлов .sql из PHP - PullRequest
       157

Загрузка файлов .sql из PHP

62 голосов
/ 29 сентября 2008

Я создаю сценарий установки для приложения, которое я разрабатываю и должен динамически создавать базы данных из PHP. У меня есть это для создания базы данных, но теперь мне нужно загрузить в несколько файлов .sql. Я планировал открыть файл и mysql_query его по строке за раз - пока я не посмотрел на файлы схемы и не понял, что это не один запрос на строку.

Итак, как мне загрузить файл sql из PHP (как phpMyAdmin с его командой import)?

Ответы [ 30 ]

67 голосов
/ 24 августа 2011
$db = new PDO($dsn, $user, $password);

$sql = file_get_contents('file.sql');

$qr = $db->exec($sql);
58 голосов
/ 07 июля 2011

phpBB использует несколько функций для разбора своих файлов. Они довольно хорошо прокомментированы (что исключение!), Поэтому вы можете легко узнать, что они делают (я получил это решение от http://www.frihost.com/forums/vt-8194.html)., вот решение, которое я часто использовал:

<php
ini_set('memory_limit', '5120M');
set_time_limit ( 0 );
/***************************************************************************
*                             sql_parse.php
*                              -------------------
*     begin                : Thu May 31, 2001
*     copyright            : (C) 2001 The phpBB Group
*     email                : support@phpbb.com
*
*     $Id: sql_parse.php,v 1.8 2002/03/18 23:53:12 psotfx Exp $
*
****************************************************************************/

/***************************************************************************
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 ***************************************************************************/

/***************************************************************************
*
*   These functions are mainly for use in the db_utilities under the admin
*   however in order to make these functions available elsewhere, specifically
*   in the installation phase of phpBB I have seperated out a couple of
*   functions into this file.  JLH
*
\***************************************************************************/

//
// remove_comments will strip the sql comment lines out of an uploaded sql file
// specifically for mssql and postgres type files in the install....
//
function remove_comments(&$output)
{
   $lines = explode("\n", $output);
   $output = "";

   // try to keep mem. use down
   $linecount = count($lines);

   $in_comment = false;
   for($i = 0; $i &lt; $linecount; $i++)
   {
      if( preg_match("/^\/\*/", preg_quote($lines[$i])) )
      {
         $in_comment = true;
      }

      if( !$in_comment )
      {
         $output .= $lines[$i] . "\n";
      }

      if( preg_match("/\*\/$/", preg_quote($lines[$i])) )
      {
         $in_comment = false;
      }
   }

   unset($lines);
   return $output;
}

//
// remove_remarks will strip the sql comment lines out of an uploaded sql file
//
function remove_remarks($sql)
{
   $lines = explode("\n", $sql);

   // try to keep mem. use down
   $sql = "";

   $linecount = count($lines);
   $output = "";

   for ($i = 0; $i &lt; $linecount; $i++)
   {
      if (($i != ($linecount - 1)) || (strlen($lines[$i]) > 0))
      {
         if (isset($lines[$i][0]) && $lines[$i][0] != "#")
         {
            $output .= $lines[$i] . "\n";
         }
         else
         {
            $output .= "\n";
         }
         // Trading a bit of speed for lower mem. use here.
         $lines[$i] = "";
      }
   }

   return $output;

}

//
// split_sql_file will split an uploaded sql file into single sql statements.
// Note: expects trim() to have already been run on $sql.
//
function split_sql_file($sql, $delimiter)
{
   // Split up our string into "possible" SQL statements.
   $tokens = explode($delimiter, $sql);

   // try to save mem.
   $sql = "";
   $output = array();

   // we don't actually care about the matches preg gives us.
   $matches = array();

   // this is faster than calling count($oktens) every time thru the loop.
   $token_count = count($tokens);
   for ($i = 0; $i &lt; $token_count; $i++)
   {
      // Don't wanna add an empty string as the last thing in the array.
      if (($i != ($token_count - 1)) || (strlen($tokens[$i] > 0)))
      {
         // This is the total number of single quotes in the token.
         $total_quotes = preg_match_all("/'/", $tokens[$i], $matches);
         // Counts single quotes that are preceded by an odd number of backslashes,
         // which means they're escaped quotes.
         $escaped_quotes = preg_match_all("/(?&lt;!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$i], $matches);

         $unescaped_quotes = $total_quotes - $escaped_quotes;

         // If the number of unescaped quotes is even, then the delimiter did NOT occur inside a string literal.
         if (($unescaped_quotes % 2) == 0)
         {
            // It's a complete sql statement.
            $output[] = $tokens[$i];
            // save memory.
            $tokens[$i] = "";
         }
         else
         {
            // incomplete sql statement. keep adding tokens until we have a complete one.
            // $temp will hold what we have so far.
            $temp = $tokens[$i] . $delimiter;
            // save memory..
            $tokens[$i] = "";

            // Do we have a complete statement yet?
            $complete_stmt = false;

            for ($j = $i + 1; (!$complete_stmt && ($j &lt; $token_count)); $j++)
            {
               // This is the total number of single quotes in the token.
               $total_quotes = preg_match_all("/'/", $tokens[$j], $matches);
               // Counts single quotes that are preceded by an odd number of backslashes,
               // which means they're escaped quotes.
               $escaped_quotes = preg_match_all("/(?&lt;!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$j], $matches);

               $unescaped_quotes = $total_quotes - $escaped_quotes;

               if (($unescaped_quotes % 2) == 1)
               {
                  // odd number of unescaped quotes. In combination with the previous incomplete
                  // statement(s), we now have a complete statement. (2 odds always make an even)
                  $output[] = $temp . $tokens[$j];

                  // save memory.
                  $tokens[$j] = "";
                  $temp = "";

                  // exit the loop.
                  $complete_stmt = true;
                  // make sure the outer loop continues at the right point.
                  $i = $j;
               }
               else
               {
                  // even number of unescaped quotes. We still don't have a complete statement.
                  // (1 odd and 1 even always make an odd)
                  $temp .= $tokens[$j] . $delimiter;
                  // save memory.
                  $tokens[$j] = "";
               }

            } // for..
         } // else
      }
   }

   return $output;
}

$dbms_schema = 'yourfile.sql';

$sql_query = @fread(@fopen($dbms_schema, 'r'), @filesize($dbms_schema)) or die('problem ');
$sql_query = remove_remarks($sql_query);
$sql_query = split_sql_file($sql_query, ';');

$host = 'localhost';
$user = 'user';
$pass = 'pass';
$db = 'database_name';

//In case mysql is deprecated use mysqli functions. 
mysqli_connect($host,$user,$pass) or die('error connection');
mysqli_select_db($db) or die('error database selection');

$i=1;
foreach($sql_query as $sql){
echo $i++;
echo "<br />";
mysql_query($sql) or die('error in query');
}

?>
46 голосов
/ 29 сентября 2008

У меня такое ощущение, что все, кто здесь ответил на этот вопрос, не знают, каково это быть разработчиком веб-приложений, который позволяет людям устанавливать приложение на своих собственных серверах. В частности, виртуальный хостинг не позволяет использовать SQL, как, например, запрос «LOAD DATA», упомянутый ранее. Большинство общих хостов также не позволяют использовать shell_exec.

Теперь, чтобы ответить на OP, лучше всего просто создать PHP-файл, содержащий ваши запросы в переменной и просто запустить их. Если вы полны решимости проанализировать файлы .sql, вам следует заглянуть в phpMyAdmin и получить некоторые идеи для получения данных из файлов .sql таким способом. Посмотрите на другие веб-приложения, в которых есть установщики, и вы увидите, что вместо того, чтобы использовать файлы .sql для своих запросов, они просто упаковывают их в файлы PHP и просто запускают каждую строку через mysql_query или что-то еще, что им нужно сделать. .

27 голосов
/ 29 сентября 2008

Самое простое решение - использовать shell_exec () для запуска клиента mysql со сценарием SQL в качестве входных данных. Это может работать немного медленнее, потому что это должно быть разветвлено, но вы можете написать код за пару минут и затем вернуться к работе над чем-то полезным. Написание сценария PHP для запуска любого сценария SQL может занять несколько недель.

Поддержка сценариев SQL сложнее, чем то, что люди здесь описывают, если только вы не уверены, что ваш сценарий содержит только подмножество функций сценариев. Ниже приведены некоторые примеры того, что может появиться в обычном сценарии SQL, что усложняет кодирование сценария для его интерпретации строка за строкой.

-- Comment lines cannot be prepared as statements
-- This is a MySQL client tool builtin command.  
-- It cannot be prepared or executed by server.
USE testdb;

-- This is a multi-line statement.
CREATE TABLE foo (
  string VARCHAR(100)
);

-- This statement is not supported as a prepared statement.
LOAD DATA INFILE 'datafile.txt' INTO TABLE foo;

-- This statement is not terminated with a semicolon.
DELIMITER //

-- This multi-line statement contains a semicolon 
-- but not as the statement terminator.
CREATE PROCEDURE simpleproc (OUT param1 INT)
BEGIN
  SELECT COUNT(*) INTO param1 FROM foo;
END
// 

Если вы поддерживаете только подмножество SQL-сценариев, исключая некоторые угловые случаи, такие как описанные выше, относительно легко написать PHP-сценарий, который читает файл и выполняет в нем операторы SQL. Но если вы хотите поддерживать любой допустимый сценарий SQL, это намного сложнее.


Смотрите также мои ответы на следующие вопросы:

10 голосов
/ 12 марта 2013

В своих проектах я использовал следующее решение:

<?php

/**
 * Import SQL from file
 *
 * @param string path to sql file
 */
function sqlImport($file)
{

    $delimiter = ';';
    $file = fopen($file, 'r');
    $isFirstRow = true;
    $isMultiLineComment = false;
    $sql = '';

    while (!feof($file)) {

        $row = fgets($file);

        // remove BOM for utf-8 encoded file
        if ($isFirstRow) {
            $row = preg_replace('/^\x{EF}\x{BB}\x{BF}/', '', $row);
            $isFirstRow = false;
        }

        // 1. ignore empty string and comment row
        if (trim($row) == '' || preg_match('/^\s*(#|--\s)/sUi', $row)) {
            continue;
        }

        // 2. clear comments
        $row = trim(clearSQL($row, $isMultiLineComment));

        // 3. parse delimiter row
        if (preg_match('/^DELIMITER\s+[^ ]+/sUi', $row)) {
            $delimiter = preg_replace('/^DELIMITER\s+([^ ]+)$/sUi', '$1', $row);
            continue;
        }

        // 4. separate sql queries by delimiter
        $offset = 0;
        while (strpos($row, $delimiter, $offset) !== false) {
            $delimiterOffset = strpos($row, $delimiter, $offset);
            if (isQuoted($delimiterOffset, $row)) {
                $offset = $delimiterOffset + strlen($delimiter);
            } else {
                $sql = trim($sql . ' ' . trim(substr($row, 0, $delimiterOffset)));
                query($sql);

                $row = substr($row, $delimiterOffset + strlen($delimiter));
                $offset = 0;
                $sql = '';
            }
        }
        $sql = trim($sql . ' ' . $row);
    }
    if (strlen($sql) > 0) {
        query($row);
    }

    fclose($file);
}

/**
 * Remove comments from sql
 *
 * @param string sql
 * @param boolean is multicomment line
 * @return string
 */
function clearSQL($sql, &$isMultiComment)
{
    if ($isMultiComment) {
        if (preg_match('#\*/#sUi', $sql)) {
            $sql = preg_replace('#^.*\*/\s*#sUi', '', $sql);
            $isMultiComment = false;
        } else {
            $sql = '';
        }
        if(trim($sql) == ''){
            return $sql;
        }
    }

    $offset = 0;
    while (preg_match('{--\s|#|/\*[^!]}sUi', $sql, $matched, PREG_OFFSET_CAPTURE, $offset)) {
        list($comment, $foundOn) = $matched[0];
        if (isQuoted($foundOn, $sql)) {
            $offset = $foundOn + strlen($comment);
        } else {
            if (substr($comment, 0, 2) == '/*') {
                $closedOn = strpos($sql, '*/', $foundOn);
                if ($closedOn !== false) {
                    $sql = substr($sql, 0, $foundOn) . substr($sql, $closedOn + 2);
                } else {
                    $sql = substr($sql, 0, $foundOn);
                    $isMultiComment = true;
                }
            } else {
                $sql = substr($sql, 0, $foundOn);
                break;
            }
        }
    }
    return $sql;
}

/**
 * Check if "offset" position is quoted
 *
 * @param int $offset
 * @param string $text
 * @return boolean
 */
function isQuoted($offset, $text)
{
    if ($offset > strlen($text))
        $offset = strlen($text);

    $isQuoted = false;
    for ($i = 0; $i < $offset; $i++) {
        if ($text[$i] == "'")
            $isQuoted = !$isQuoted;
        if ($text[$i] == "\\" && $isQuoted)
            $i++;
    }
    return $isQuoted;
}

function query($sql)
{
    global $mysqli;
    //echo '#<strong>SQL CODE TO RUN:</strong><br>' . htmlspecialchars($sql) . ';<br><br>';
    if (!$query = $mysqli->query($sql)) {
        throw new Exception("Cannot execute request to the database {$sql}: " . $mysqli->error);
    }
}

set_time_limit(0);

$mysqli = new mysqli('localhost', 'root', '', 'test');
$mysqli->set_charset("utf8");

header('Content-Type: text/html;charset=utf-8');
sqlImport('import.sql');

echo "Peak MB: ", memory_get_peak_usage(true)/1024/1024;

Пиковое использование памяти в тестовом файле sql (41 МБ): 3,25 МБ

10 голосов
/ 29 сентября 2008

mysqli может выполнять несколько запросов, разделенных ;

Вы можете прочитать весь файл и запустить все сразу, используя mysqli_multi_query()

Но я буду первым, кто скажет, что это не самое элегантное решение.

5 голосов
/ 02 апреля 2015

Поскольку я не могу комментировать ответ, остерегайтесь использовать следующее решение:

$db = new PDO($dsn, $user, $password);

$sql = file_get_contents('file.sql');

$qr = $db->exec($sql);

В PHP PDO есть ошибка https://bugs.php.net/bug.php?id=61613

db->exec('SELECT 1; invalidstatement; SELECT 2');

не выдаст ошибку или вернет false (проверено на PHP 5.5.14).

4 голосов
/ 17 июля 2012

Обновленное решение от Plahcinski. В качестве альтернативы вы можете использовать fopen и fread для больших файлов:

$fp = file('database.sql', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$query = '';
foreach ($fp as $line) {
    if ($line != '' && strpos($line, '--') === false) {
        $query .= $line;
        if (substr($query, -1) == ';') {
            mysql_query($query);
            $query = '';
        }
    }
}
4 голосов
/ 29 сентября 2008

Я бы посоветовал взглянуть на исходный код PHPMyBackup. Это автоматический загрузчик PHP SQL. Вы обнаружите, что mysql_query загружает только один запрос за раз, а такие проекты, как PHPMyAdmin и PHPMyBackup, уже проделали тяжелую работу по правильному разбору SQL. Пожалуйста, не изобретайте это колесо: P

4 голосов
/ 29 сентября 2008

Вы уверены, что это не один запрос на строку? Ваш текстовый редактор может переносить строки, но в действительности каждый запрос может быть в одной строке.

В любом случае, метод Олле кажется лучшим. Если у вас есть причины для выполнения запросов по одному, вы должны иметь возможность читать в файле строку за строкой, а затем использовать точку с запятой в конце каждого запроса для разделения. Вы гораздо лучше читаете файл за строкой, чем пытаетесь разбить огромную строку, так как это будет намного лучше для памяти вашего сервера. Пример:

$query  = '';
$handle = @fopen("/sqlfile.sql", "r");

if ($handle) {
    while (!feof($handle)) {
        $query.= fgets($handle, 4096);

        if (substr(rtrim($query), -1) == ';') {
            // ...run your query, then unset the string
            $query = '';
        }
    }

    fclose($handle);
}

Очевидно, что вам нужно учитывать транзакции и все остальное, если вы выполняете большое количество запросов в пакете, но, вероятно, это не имеет большого значения для сценария новой установки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...