Хранение изображений и других двоичных данных

Задача
Вы хотите хранить в MySQL изображения.

Решение
Это несложно, если вы правильно закодируете данные.

Обсуждение
На веб-сайтах можно отображать не только текст, но и различные виды двоичных данных, такие как изображения, звуки, PDF-документы и так далее. Самым распространенным видом двоичных данных являются изображения, а базы данных подходят для их хранения, поэтому периодически возникает вопрос «Как хранить изображения в MySQL?». Многие сразу ответят: «Не делайте этого!», и на то есть ряд причин, некоторые из них приведены в примечании «Следует ли хранить изображения в базе данных?». Поскольку уметь работать с двоичными данными полезно, в этом разделе показано, как хранить изображения в MySQL. Но поскольку это не всегда удачная идея, рассказано и о том, как хранить изображения в файловой системе.

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

Одна из причин, по которой хранение изображений представляется чем-то более сложным, чем хранение текстовых строк или чисел, заключается в том, что значение изображения сложно ввести вручную.


Например, легко использовать mysql для выполнения предложения INSERT, вставляющего число типа 3.48 или строку Je voudrais une bicyclette rouge, но изображения содержат двоичные данные, и оперировать их значениями нелегко. Придется найти какой-то другой выход. Ваши варианты:

• Использовать функцию LOAD_FILE().

• Написать функцию, которая читает файл изображения и формирует для вас соответствующее предложение INSERT.

Сохранение изображений с помощью функции LOAD_FILE()
Функция LOAD_FILE() принимает аргумент, указывающий, какой файл следует прочитать и сохранить в базе данных. Например, хранящееся в /tmp/myimage.png изображение можно сохранить в таблице так:

mysql> INSERT INTO mytbl (image_data) VALUES(LOAD_FILE('/tmp/myimage.png'));

Для сохранения изображений в MySQL при помощи LOAD_FILE() требуется выполнение нескольких условий:

• Файл изображения должен располагаться на хосте сервера MySQL.

• Файл должен быть доступен серверу для чтения.

• Вы должны обладать привилегией FILE.

То есть функция LOAD_FILE() доступна не всем пользователям MySQL.

Сохранение изображений с помощью сценария
Если использование функции LOAD_FILE() недоступно или нежелательно, вы можете написать небольшую программу, которая будет загружать ваши изображения.


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

Сценарий store_image.pl запускается из командной строки и сохраняет файл изображения для дальнейшего использования. Этот сценарий не держится ни одной из сторон в дебатах о хранении изображений в базе данных или файловой системе. Вместо этого он реализует оба способа. Конечно, это удваивает объем пространства для хранения, так что, адаптируя сценарий для собственного использования, вы можете оставить в нем только то, что соответствует предпочитаемому вами способу хранения изображений. Необходимые изменения будут рассмотрены в конце раздела.

Сценарий store_image.pl работает с таблицей image, в которой есть столбцы идентификатора, имени файла и MIME-типа изображения, а также столбец собственно данных изображения:

CREATE TABLE image
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT, # идентификатор изображения
name VARCHAR(30) NOT NULL, # имя файла изображения
type VARCHAR(20) NOT NULL, # MIME-тип изображения
data MEDIUMBLOB NOT NULL, # данные изображения
PRIMARY KEY (id), # id и name уникальны
UNIQUE (name)
);

Столбец name указывает имя файла изображения в каталоге, в котором изображения хранятся в файловой системе.


Столбец data имеет тип MEDIUMBLOB и подходит для изображений размером не более 16Мбайт. Если вы предполагаете хранить более объемистые изображения, используйте столбец типа LONGBLOB.

Можно использовать столбец name для хранения полных путей к изображениям в базе данных, но если поместить их все в один каталог, то можно хранить имена относительно этого каталога, тогда значения name будут занимать меньше места. Сценарий store_image.pl именно так и поступает, но, естественно, ему нужно откуда-то узнать путь к каталогу для хранения изображений. Для этого применяется переменная $image_dir. Вы проверяете значение этой переменной и (при необходимости) изменяете его перед запуском сценария. Значение по умолчанию соответствует тому каталогу, гдея предпочитаю хранить изображения, но вы можете изменить его согласно собственным пожеланиям. Убедитесь в том, что указанный каталог существует (или создайте его) перед запуском сценария. Вам также необходимо проверить и, возможно, изменить путь к каталогу изображений в сценарии display_image.pl, который будет рассмотрен далее в этой главе.

Сценарий store_image.pl выглядит так:

#! /usr/bin/perl -w
# store_image.pl – читает файл изображения, сохраняет в таблице
# и в файловой системе. (Обычно изображение сохраняется только в каком-то
# одном месте, сценарий же приводит два способа.)
use strict;
use lib qw(/usr/local/apache/lib/perl);
use Fcntl; # для O_RDONLY, O_WRONLY, O_CREAT
use FileHandle;
use Cookbook;
# Значения по умолчанию для каталога хранения изображения и разделителя пути
# (ИЗМЕНИТЬ ЗДЕСЬ, ЕСЛИ ТРЕБУЕТСЯ)
my $image_dir = "/usr/local/apache/htdocs/mcb/images";
my $path_sep = "/";
# Установка каталога и разделителя пути для Windows/DOS
if ($^O =~ /^MSWin/i || $^O =~ /^dos/)
{
$image_dir = "D:\\apache\\htdocs\\mcb\\images";
$path_sep = "\\";
}
-d $image_dir or die "$0: image directory ($image_dir)\ndoes not exist\n";
# Вывод справочного сообщения в случае, если сценарий вызван некорректно(@ARGV == 2 || @ARGV == 3) or die < Usage: $0 image_file mime_type [image_name]
image_file = name of the image file to store
mime_time = the image MIME type (e.g., image/jpeg or image/png)
image_name = alternate name to give the image
image_name is optional; if not specified, the default is the
image file basename.
EOF
my $file_name = shift (@ARGV); # имя файла изображения
my $mime_type = shift (@ARGV); # MIME-тип изображения
my $image_name = shift (@ARGV); # название изображения (необязательно)
# если не указано название изображения, использовать исходное имя
# (разрешить разделитель / или \)
($image_name = $file_name) =~ s|.*[/\\]|| unless defined $image_name;
my $fh = new FileHandle;
my ($size, $data);
sysopen ($fh, $file_name, O_RDONLY)
or die "Cannot read $file_name: $!\n";
binmode ($fh); # для работы в двоичном режиме
$size = (stat ($fh))[7];
sysread ($fh, $data, $size) == $size
or die "Failed to read entire file $file_name: $!\n";
$fh->close ();
# Сохранить файл изображения в файловой системе в каталоге $image_dir.
# (Перезаписать файл, если он уже существует.)
my $image_path = $image_dir . $path_sep . $image_name;
sysopen ($fh, $image_path, O_WRONLY|O_CREAT)
or die "Cannot open $image_path: $!\n";
binmode ($fh); # для работы в двоичном режиме
syswrite ($fh, $data, $size) == $size
or die "Failed to write entire image file $image_path: $!\n";
$fh->close ();
# Сохранить изображение в таблице базы данных. (Использовать REPLACE
# для удаления старых изображений с таким же именем.)
my $dbh = Cookbook::connect ();
$dbh->do ("REPLACE INTO image (name,type,data) VALUES(?,?,?)",
undef,
$image_name, $mime_type, $data);
$dbh->disconnect ();
exit (0);

Если вы вызываете сценарий без аргументов, он выводит короткое справочное сообщение. В противном случае он требует ввести два аргумента: имя файла изображения и MIME-тип изображения. По умолчанию имя исходногофайла (последняя составляющая пути) используется и как название изображения, хранящегося в базе данных и в каталоге. Если вы хотите использовать другое имя, укажите его как третий необязательный аргумент.

Сценарий достаточно прост. Он выполняет следующие действия:

1. Проверяет, правильное ли количество аргументов указано, и устанавливает по ним некоторые переменные.

2. Убеждается в наличии каталога изображений. В случае его отсутствия сценарий не может продолжать работу.

3. Открывает и читает файл изображения.

4. Сохраняет изображение в файле в соответствующем каталоге.

5. Сохраняет запись, содержащую идентификационную информацию и данные изображения, в таблице image.

Сценарий store_image.pl использует предложение REPLACE, а не INSERT, так что для замены старого изображения новым просто загрузите его с тем же именем. В запросе не указано значение для столбца id; это столбец AUTO_INCREMENT, поэтому MySQL автоматически присваивает ему уникальный номер последовательности. (Имейте в виду, что если вы заменяете изображение, загружая новое с тем же именем, то предложение REPLACE сгенерирует новое значение id. Если вы хотите сохранить старое значение, необходимо сначала выполнить SELECT, чтобы узнать, существует ли указанное имя, затем изменить REPLACE, указав имеющееся значение id, если запись была обнаружена, и NULL – в противном случае.)

Предложение REPLACE, сохраняющее информацию об изображении в MySQL, выглядит вполне привычно:

$dbh->do ("REPLACE INTO image (name,type,data) VALUES(?,?,?)",
undef,
$image_name, $mime_type, $data);

Если вы посмотрите на предложение, пытаясь обнаружить какой-то специальный указатель того, как следует обрабатывать двоичные данные, то ничего не увидите. Переменная $data, содержащая изображение, никаким специальным образом не обрабатывается. Запрос одинаково ссылается на все столбцы, используя символы-заполнители ?, с передачей значений в конце вызова do(). Есть и другой способ получения такого же результата – явно кодировать значения столбцов, а затем вставлять их прямо в строку запроса:

$image_name = $dbh->quote ($image_name);
$mime_type = $dbh->quote ($mime_type);
$data = $dbh->quote ($data);
$dbh->do ("REPLACE INTO image (name,type,data)
VALUES($image_name,$mime_type,$data)");

Многие склонны неоправданно усложнять работу с изображениями. Если вы корректно обрабатываете изображения в запросе, используя заполнители или кодируя их, проблем не возникнет. В противном случае возникнут ошибки. Все просто. Нет никаких отличий от обработки других видов данных,в том числе и текста. В конце концов, если вставить в запрос элемент текста, содержащий кавычки или другие специальные символы, и не экранировать их, то запрос выскажет все, что он о вас думает. Так что использование заполнителей или кодирование присуще не только изображениям, а необходимо для любых данных. Повторяйте за мной: «Я буду всегда использовать заполнители или кодировать значения столбцов. Всегда, всегда, всегда!» (Сказав это, уточню, что если у вас есть достоверные данные о том, что указанное значение, например, является целочисленным, то вы можете нарушить данное обещание. Однако применение такого правила никогда не повредит.)

Для того чтобы опробовать сценарий, перейдите в каталог apache/images дистрибутива recipes. Каталог содержит сценарий store_image.pl, а в подкаталоге flags содержится несколько тестовых изображений (это национальные флаги различных стран). Чтобы загрузить одно из таких изображений, запустите сценарий в UNIX так:

% ./store_image.pl flags/iceland.jpg image/jpeg

Тем, кто работает в Windows, следует поступить так:

C:\> store_image.pl flags\iceland.jpg image/jpeg

Сценарий store_image.pl занимается сохранением изображения, а в следующем разделе будет рассказано об извлечении изображений и отправке их через Сеть. А как насчет удаления изображений? Напишите утилиту, удаляющую ненужные изображения, самостоятельно. Если вы храните изображения в файловой системе, помните о необходимости удаления как записи базы данных, так и файла, на который эта запись указывает.

Сценарий store_image.pl сохраняет каждое изображение и в базе данных, и в файловой системе для демонстрации обоих методов, что, естественно, делает его неэффективным. Ранее я уже говорил, что если вы используете этот сценарий как основу для собственных приложений, вам следует изменить его так, чтобы он реализовывал только один из методов хранения изображений, но не оба сразу. Изменения должны быть такими:

• Чтобы сценарий сохранял изображения только в MySQL, не определяйте каталог изображений и удалите код, проверяющий наличие каталога, а затем записывающий в него файлы изображений.

• Чтобы сценарий сохранял изображения только в файловой системе, удалите столбец данных из таблицы image и измените предложение REPLACE так, чтобы оно не ссылалось на этот столбец.

Оцените статью: (0 голосов)
0 5 0

Статьи из раздела MySQL на эту тему:
Извлечение изображений и других двоичных данных
Использование результатов запроса для загрузки файлов
Представление результатов запроса в виде абзацев
Представление результатов запроса в виде гиперссылок
Представление результатов запроса в виде списков