Як налагодити запити до баз даних PDO?


140

Перш ніж перейти до PDO, я створив SQL запити в PHP шляхом об'єднання рядків. Якщо я отримав помилку синтаксису бази даних, я міг би просто повторити остаточний рядок запиту SQL, спробувати його в базі даних і налаштувати його, поки я не виправлю помилку, а потім вставте її назад у код.

Підготовлені заяви PDO швидші, якісніші та безпечніші, але одне мене турбує: я ніколи не бачу остаточного запиту, коли він надсилається до бази даних. Коли я отримую помилки щодо синтаксису в моєму журналі Apache або в моєму власному файлі журналу (я реєструю помилки всередині catchблоку), я не бачу запит, який їх викликав.

Чи є спосіб зафіксувати повний SQL-запит, надісланий PDO до бази даних, і записати його у файл?


4
Він є зареєстрованим в файлі: /var/log/mysql/*. Параметри, пов'язані з PDO, не можуть викликати синтаксичні помилки, тому все, що вам потрібно, - це підготовлений SQL-запит.
Xeoncross

1
дивіться код у stackoverflow.com/questions/210564/… (не у прийнятій відповіді). Не те, щоб було опубліковано кілька оновлень.
Мауг каже, що повернемо Моніку

1
Простий однолінійний через композитор: github.com/panique/pdo-debug
Sliq

2
Відповідь Ксеонкросса мені допомогла. Ось стаття, яка пояснює, як увімкнути цю функцію. На багатьох серверних установках це значення за замовчуванням. pontikis.net/blog/how-and-when-to-enable-mysql-logs
mrbinky3000

2
Спробуйте зvar_dump($pdo_instance->debugDumpParams())
Даніель Петровалієв

Відповіді:


99

Ви говорите так:

Я ніколи не бачу остаточного запиту, коли він надсилається до бази даних

Ну, насправді, при використанні підготовлених висловлювань, немає такого поняття, як " остаточний запит " :

  • Спочатку заява надсилається до БД і готується там
    • База даних аналізує запит і будує його внутрішнє представлення
  • І коли ви прив'язуєте змінні та виконуєте оператор, у базу даних надсилаються лише змінні
    • А база даних "вводить" значення у своє внутрішнє подання заяви


Отже, щоб відповісти на ваше запитання:

Чи є спосіб зафіксувати повний SQL-запит, надісланий PDO до бази даних, і записати його у файл?

Ні: оскільки " повного запиту SQL " ніде немає, немає способу захопити його.


Найкраще, що ви можете зробити для налагодження - це "реконструювати" "справжній" SQL-запит, вводячи значення в рядок SQL оператора.

Що я зазвичай роблю в таких ситуаціях:

  • перегукуйте код SQL, який відповідає оператору, із заповненнями
  • і використовувати var_dump (або еквівалент) відразу після, щоб відобразити значення параметрів
  • Це, як правило, достатньо, щоб побачити можливу помилку, навіть якщо у вас немає жодного "реального" запиту, який ви можете виконати.

Це не дуже добре, якщо мова йде про налагодження - але це ціна підготовлених заяв та переваги, які вони приносять.


1
Чудове пояснення - спасибі. Мабуть, у мене були лише нечіткі уявлення про те, як це працює. Я припускаю, що коли заява готується, отриманий об’єкт містить хеш або числовий ідентифікатор, який може бути відправлений назад до бази даних з параметрами для підключення.
Натан Лонг,

Ласкаво просимо :-) ;;; Я не знаю, як це детально реалізовано, але я вважаю, що це щось подібне - результат все одно такий; це одна з приємних речей з підготовленими висловлюваннями: якщо вам доведеться виконувати один і той же запит багато разів, він буде надісланий в БД і підготовлений один раз: для кожного виконання будуть надсилатися лише дані.
Паскаль МАРТИН

1
Оновлення: Аарон Паттерсон згадував на Railsconf 2011, що він додав до Rails більше підготовлених заяв, але що користь у PostgreSQL набагато важче, ніж у MySQL. Він сказав, що це тому, що MySQL насправді не створює план запитів, поки ви не виконаєте підготовлений запит.
Натан Лонг

85

Дивлячись у журнал бази даних

Хоча Pascal MARTIN вірно, що PDO не надсилає повний запит до бази даних відразу, ryeguy пропозиція використовувати функцію реєстрації БД фактично дозволила мені бачити повний запит як зібраний і виконаний базою даних.

Ось як: (Ці інструкції призначені для MySQL на машині Windows - ваш пробіг може відрізнятися)

  • У my.iniпід [mysqld]розділом додайте logкоманду, як-отlog="C:\Program Files\MySQL\MySQL Server 5.1\data\mysql.log"
  • Перезавантажте MySQL.
  • Він почне реєструвати кожен запит у цьому файлі.

Цей файл швидко зростатиме, тому обов'язково видаліть його та вимкніть журнал, коли закінчите тестування.


1
Лише зауваження - мені довелося уникнути косої частини в моєму.іні. Отже, мій запис виглядав приблизно як log = "C: \\ temp \\ MySQL \\ mysql.log".
Джим

4
Це може працювати залежно від налаштування PDO::ATTR_EMULATE_PREPARES. Дивіться цей відповідь для отримання додаткової інформації: stackoverflow.com/questions/10658865/#answer-10658929
webbiedave

23
Я ненавиджу PDO через це.
Салман

1
@webbiedave - о, вау! Ваша відповідна відповідь означає, що моя відповідь працює лише тоді, коли PDO працює не оптимально, а швидше надсилає весь запит на зворотну сумісність зі старою версією MySQL або старим драйвером. Цікаво.
Натан Лонг

13
У MySQL 5.5+ вам потрібно general_logзамість log. Дивіться dev.mysql.com/doc/refman/5.5/uk/query-log.html
Адріан Макнейл

18

Звичайно, ви можете налагоджувати в цьому режимі {{ PDO::ATTR_ERRMODE }} Просто додайте новий рядок перед запитом, тоді ви покажете рядки налагодження.

$db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$db->query('SELECT *******');  

Ви б не телефонували, ->queryколи використовуєте підготовлені заяви?
EoghanM

Дякую, що мені дуже допомогло! :)
розрив

17

Ймовірно, що ви хочете зробити, це використовувати debugDumpParams () на ручці оператора. Ви можете це запустити будь-коли після прив’язки значень до підготовленого запиту (немає необхідності execute()в операторі).

Він не будує підготовлену заяву для вас, але покаже ваші параметри.


2
Єдина проблема полягає в тому, що він виводить налагодження замість того, щоб зберігати його всередині, не виконуючи його. Я не можу це зафіксувати таким чином.
Рікардо Мартінс

3
Ви можете використовувати буферизацію виводу (ob_start () ...) для зберігання виводу та реєстрації його.
Креніо

bugs.php.net/bug.php?id=52384 зафіксовано в 7.1, ви можете бачити значення :) трохи пізно, але це php
Sander Visser

12

Стара публікація, але, можливо, хтось вважатиме це корисним;

function pdo_sql_debug($sql,$placeholders){
    foreach($placeholders as $k => $v){
        $sql = preg_replace('/:'.$k.'/',"'".$v."'",$sql);
    }
    return $sql;
}

1
Про подібну функцію, яка також може обробляти числові параметри, дивіться мою відповідь (завдяки коментатору на php.net).
Метт Браун

9

Ось функція, щоб побачити, яким буде ефективний SQL, пояснюється з коментаря "Марк" на php.net :

function sql_debug($sql_string, array $params = null) {
    if (!empty($params)) {
        $indexed = $params == array_values($params);
        foreach($params as $k=>$v) {
            if (is_object($v)) {
                if ($v instanceof \DateTime) $v = $v->format('Y-m-d H:i:s');
                else continue;
            }
            elseif (is_string($v)) $v="'$v'";
            elseif ($v === null) $v='NULL';
            elseif (is_array($v)) $v = implode(',', $v);

            if ($indexed) {
                $sql_string = preg_replace('/\?/', $v, $sql_string, 1);
            }
            else {
                if ($k[0] != ':') $k = ':'.$k; //add leading colon if it was left out
                $sql_string = str_replace($k,$v,$sql_string);
            }
        }
    }
    return $sql_string;
}

Чому "Марк" використовує двокрапку до $ k str_replace(":$k" ....? Асоціативні індекси вже є в масиві $ params.
Алан

Хороше запитання ... це може пояснити: stackoverflow.com/questions/9778887 / ... . Особисто я використовував цю функцію для налагодження запитів доктрини, і я думаю, що Doctrine використовує нумеровані, а не названі параметри, тому я не помітив цієї проблеми. Я оновив функцію, щоб вона тепер працювала з або без провідних колонок.
Метт Браун

зауважте, що це рішення замінює :name_longна :name. Принаймні, якщо :nameприходить раніше :name_long. Підготовлені оператори MySQL можуть правильно впоратися з цим, тому нехай вас це не бентежить.
Zim84

8

Ні. PDO-запити не готуються на стороні клієнта. PDO просто відправляє запит SQL та параметри на сервер бази даних. База даних - це те, що робить підміна (з ?). У вас є два варіанти:

  • Використовуйте функцію реєстрації вашої БД (але навіть тоді вона зазвичай відображається у вигляді двох окремих висловлювань (тобто "не остаточний") принаймні з Postgres)
  • Виведіть SQL-запит та параметри та складіть його разом

Я ніколи не думав перевіряти журнал БД. Я розсуваюся в каталозі MySQL і не бачу файлів журналів, але, можливо, реєстрація - це варіант, який я повинен десь увімкнути.
Натан Лонг

Так, ви повинні увімкнути його. Я не знаю специфіки, але за замовчуванням вона не реєструє кожен запит.
ryeguy

5

майже нічого не було сказано про відображення помилок, крім журналів помилок перевірки, але є досить корисна функціональність:

<?php
/* Provoke an error -- bogus SQL syntax */
$stmt = $dbh->prepare('bogus sql');
if (!$stmt) {
    echo "\PDO::errorInfo():\n";
    print_r($dbh->errorInfo());
}
?>

( посилання на джерело )

Зрозуміло, що цей код може бути змінений, щоб використовуватись як повідомлення про виключення або будь-який інший вид помилок


2
Це неправильний шлях. PDO досить розумний, щоб зробити цей код марним. Просто скажіть, щоб кидати винятки на помилки. PHP зробить все інше, набагато краще, ніж ця обмежена функція. Також, будь ласка , навчіться не друкувати всі помилки безпосередньо у браузері. Є кращі способи.
Твій здоровий глузд

3
це офіційна документація, і звичайно, ніхто не збирався надрукувати цю помилку у виробництві, знову ж таки це приклад з офіційного сайту (php.net), дивіться посилання під прикладом коду. І напевно, набагато краще - використовувати додаткові параметри $ db-> setAttribute (PDO :: ATTR_ERRMODE, PDO :: ERRMODE_EXCEPTION) у межах PDO, але, на жаль, ви не могли отримати доступ до цього коду
Zippp

4

наприклад, у вас є ця заявка pdo:

$query="insert into tblTest (field1, field2, field3)
values (:val1, :val2, :val3)";
$res=$db->prepare($query);
$res->execute(array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
));

тепер ви можете отримати виконаний запит, визначивши такий масив:

$assoc=array(
  ':val1'=>$val1,
  ':val2'=>$val2,
  ':val3'=>$val3,
);
$exQuery=str_replace(array_keys($assoc), array_values($assoc), $query);
echo $exQuery;

1
Працювали для мене. Ви маєте помилку у другому зразку коду: ));повинен бути );(лише одна кругла дужка).
Ясом Дотнет

2

Шукаючи Інтернет, я вважав це прийнятним рішенням. Використовується інший клас замість PDO, а функції PDO викликаються через магічні виклики функцій. Я не впевнений, що це створює серйозні проблеми з роботою. Але його можна використовувати, поки в PDO не буде додана можливість розважливого ведення журналу.

Отже, відповідно до цього потоку , ви можете написати обгортку для підключення PDO, яке може ввійти та викинути виняток, коли ви отримаєте помилку.

Ось простий приклад:

class LoggedPDOSTatement extends PDOStatement    {

function execute ($array)    {
    parent::execute ($array);
    $errors = parent::errorInfo();
    if ($errors[0] != '00000'):
        throw new Exception ($errors[2]);
    endif;
  }

}

тому ви можете використовувати цей клас замість PDOStatement:

$this->db->setAttribute (PDO::ATTR_STATEMENT_CLASS, array ('LoggedPDOStatement', array()));

Тут згадана реалізація декораторів PDO:

class LoggedPDOStatement    {

function __construct ($stmt)    {
    $this->stmt = $stmt;
}

function execute ($params = null)    {
    $result = $this->stmt->execute ($params); 
    if ($this->stmt->errorCode() != PDO::ERR_NONE):
        $errors = $this->stmt->errorInfo();
        $this->paint ($errors[2]);
    endif;
    return $result;
}

function bindValue ($key, $value)    {
    $this->values[$key] = $value;    
    return $this->stmt->bindValue ($key, $value);
}

function paint ($message = false)    {
    echo '<pre>';
    echo '<table cellpadding="5px">';
    echo '<tr><td colspan="2">Message: ' . $message . '</td></tr>';
    echo '<tr><td colspan="2">Query: ' . $this->stmt->queryString . '</td></tr>';
    if (count ($this->values) > 0):
    foreach ($this->values as $key => $value):
    echo '<tr><th align="left" style="background-color: #ccc;">' . $key . '</th><td>' . $value . '</td></tr>';
    endforeach;
    endif;
    echo '</table>';
    echo '</pre>';
}

function __call ($method, $params)    {
    return call_user_func_array (array ($this->stmt, $method), $params); 
}

}

2

Щоб увімкнути MySQL в WAMP , вам потрібно буде відредагувати my.ini (наприклад, під wamp \ bin \ mysql \ mysql5.6.17 \ my.ini)

і додати до [mysqld]:

general_log = 1
general_log_file="c:\\tmp\\mysql.log"

1

Ось функція, яку я зробив, щоб повернути SQL-запит з "вирішеними" параметрами.

function paramToString($query, $parameters) {
    if(!empty($parameters)) {
        foreach($parameters as $key => $value) {
            preg_match('/(\?(?!=))/i', $query, $match, PREG_OFFSET_CAPTURE);
            $query = substr_replace($query, $value, $match[0][1], 1);
        }
    }
    return $query;
    $query = "SELECT email FROM table WHERE id = ? AND username = ?";
    $values = [1, 'Super'];

    echo paramToString($query, $values);

Якщо припустити, що ви виконуєте так

$values = array(1, 'SomeUsername');
$smth->execute($values);

Ця функція НЕ додає лапок до запитів, але виконує завдання для мене.


0

Проблема, з якою у мене виникло рішення щодо вилучення PDO-винятків для цілей налагодження, полягає в тому, що він зафіксував виключення виключень PDO (duh), але не вловив синтаксичні помилки, які були зареєстровані як помилки php (я не впевнений, чому це так, але " чому "не має значення для рішення). Всі мої дзвінки в PDO походять з одного модельного класу таблиці, який я розширив для всіх моїх взаємодій зі всіма таблицями ... це складні речі, коли я намагався налагодити код, оскільки помилка зареєструвала рядок php-коду, де був мій виклик виконання зателефонував, але не сказав мені, звідки насправді дзвонили. Я вирішив цю проблему за допомогою наступного коду:

/**
 * Executes a line of sql with PDO.
 * 
 * @param string $sql
 * @param array $params
 */
class TableModel{
    var $_db; //PDO connection
    var $_query; //PDO query

    function execute($sql, $params) { 
        //we're saving this as a global, so it's available to the error handler
        global $_tm;
        //setting these so they're available to the error handler as well
        $this->_sql = $sql;
        $this->_paramArray = $params;            

        $this->_db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->_query = $this->_db->prepare($sql);

        try {
            //set a custom error handler for pdo to catch any php errors
            set_error_handler('pdoErrorHandler');

            //save the table model object to make it available to the pdoErrorHandler
            $_tm = $this;
            $this->_query->execute($params);

            //now we restore the normal error handler
            restore_error_handler();
        } catch (Exception $ex) {
            pdoErrorHandler();
            return false;
        }            
    }
}

Отже, наведений вище код виявляє BOTH PDO винятки І помилки синтаксису php та трактує їх так само. Мій обробник помилок виглядає приблизно так:

function pdoErrorHandler() {
    //get all the stuff that we set in the table model
    global $_tm;
    $sql = $_tm->_sql;
    $params = $_tm->_params;
    $query = $tm->_query;

    $message = 'PDO error: ' . $sql . ' (' . implode(', ', $params) . ") \n";

    //get trace info, so we can know where the sql call originated from
    ob_start();
    debug_backtrace(); //I have a custom method here that parses debug backtrace, but this will work as well
    $trace = ob_get_clean();

    //log the error in a civilized manner
    error_log($message);

    if(admin(){
        //print error to screen based on your environment, logged in credentials, etc.
        print_r($message);
    }
}

Якщо хтось має кращі ідеї щодо отримання відповідної інформації до мого обробника помилок, ніж встановлення моделі таблиці як глобальної змінної, я був би радий почути її та відредагувати свій код.


0

цей код для мене чудово працює:

echo str_replace(array_keys($data), array_values($data), $query->queryString);

Не забудьте замінити $ data та $ query вашими іменами


0

Я використовую цей клас для налагодження PDO (з Log4PHP )

<?php

/**
 * Extends PDO and logs all queries that are executed and how long
 * they take, including queries issued via prepared statements
 */
class LoggedPDO extends PDO
{

    public static $log = array();

    public function __construct($dsn, $username = null, $password = null, $options = null)
    {
        parent::__construct($dsn, $username, $password, $options);
    }

    public function query($query)
    {
        $result = parent::query($query);
        return $result;
    }

    /**
     * @return LoggedPDOStatement
     */
    public function prepare($statement, $options = NULL)
    {
        if (!$options) {
            $options = array();
        }
        return new \LoggedPDOStatement(parent::prepare($statement, $options));
    }
}

/**
 * PDOStatement decorator that logs when a PDOStatement is
 * executed, and the time it took to run
 * @see LoggedPDO
 */
class LoggedPDOStatement
{

    /**
     * The PDOStatement we decorate
     */
    private $statement;
    protected $_debugValues = null;

    public function __construct(PDOStatement $statement)
    {
        $this->statement = $statement;
    }

    public function getLogger()
    {
        return \Logger::getLogger('PDO sql');
    }

    /**
     * When execute is called record the time it takes and
     * then log the query
     * @return PDO result set
     */
    public function execute(array $params = array())
    {
        $start = microtime(true);
        if (empty($params)) {
            $result = $this->statement->execute();
        } else {
            foreach ($params as $key => $value) {
                $this->_debugValues[$key] = $value;
            }
            $result = $this->statement->execute($params);
        }

        $this->getLogger()->debug($this->_debugQuery());

        $time = microtime(true) - $start;
        $ar = (int) $this->statement->rowCount();
        $this->getLogger()->debug('Affected rows: ' . $ar . ' Query took: ' . round($time * 1000, 3) . ' ms');
        return $result;
    }

    public function bindValue($parameter, $value, $data_type = false)
    {
        $this->_debugValues[$parameter] = $value;
        return $this->statement->bindValue($parameter, $value, $data_type);
    }

    public function _debugQuery($replaced = true)
    {
        $q = $this->statement->queryString;

        if (!$replaced) {
            return $q;
        }

        return preg_replace_callback('/:([0-9a-z_]+)/i', array($this, '_debugReplace'), $q);
    }

    protected function _debugReplace($m)
    {
        $v = $this->_debugValues[$m[0]];

        if ($v === null) {
            return "NULL";
        }
        if (!is_numeric($v)) {
            $v = str_replace("'", "''", $v);
        }

        return "'" . $v . "'";
    }

    /**
     * Other than execute pass all other calls to the PDOStatement object
     * @param string $function_name
     * @param array $parameters arguments
     */
    public function __call($function_name, $parameters)
    {
        return call_user_func_array(array($this->statement, $function_name), $parameters);
    }
}

0

Я створив сучасний проект / сховище для композиторів для саме цього:

pdo-налагодження

Знайдіть домашню програму GitHub проекту тут , дивіться повідомлення в блозі, де це пояснюється тут . Один рядок, який потрібно додати у ваш composer.json, а потім ви можете використовувати його так:

echo debugPDO($sql, $parameters);

$ sql - це необроблений оператор SQL, $ параметри - це масив ваших параметрів: Ключ - це ім'я заповнювача (": user_id") або номер безіменного параметра ("?"), значення є ... ну, значення.

Логіка: Цей сценарій буде просто розміщувати параметри та замінювати їх у наданій рядку SQL. Суперпростий, але надто ефективний для 99% випадків використання. Примітка. Це просто основна емуляція, а не реальна налагодження PDO (оскільки це неможливо, оскільки PHP надсилає необроблений SQL та параметри на сервер MySQL відокремлено).

Велике спасибі до bigwebguy і Майку з нитки StackOverflow Отримання рядка сирої SQL запиту з PDO для запису в основному всі основні функції за цей сценарієм. Великий вгору!


0

Як налагодити запити до бази даних PDO mysql в Ubuntu

TL; DR Запишіть усі запити та оберіть журнал mysql.

Ці вказівки призначені для моєї установки Ubuntu 14.04. Команда видати, lsb_release -aщоб отримати вашу версію. Установка може бути різною.

Увімкніть реєстрацію в mysql

  1. Перейдіть на свій cmd-рядок сервера розробки
  2. Зміна каталогів cd /etc/mysql. Ви повинні побачити файл з назвоюmy.cnf . Це файл, який ми змінимо.
  3. Переконайтесь, що ви в потрібному місці, ввівши cat my.cnf | grep general_log. Це фільтрує my.cnfфайл для вас. Ви повинні побачити два записи: #general_log_file = /var/log/mysql/mysql.log&& #general_log = 1.
  4. Відкупіруйте ці два рядки та збережіть їх за допомогою редактора на вибір.
  5. Перезапуск MySQL: sudo service mysql restart.
  6. Можливо, вам також знадобиться перезапустити веб-сервер. (Я не можу згадати послідовність, яку я використовував). Для моєї установки, це nginx : sudo service nginx restart.

Хороша робота! Ви все налаштовані. Тепер все, що вам потрібно зробити, це зберегти файл журналу, щоб ви могли бачити запити PDO, які ваш додаток робить в режимі реального часу.

Хвост журналу, щоб побачити ваші запити

Введіть цей cmd tail -f /var/log/mysql/mysql.log.

Ваш результат буде виглядати приблизно так:

73 Connect  xyz@localhost on your_db
73 Query    SET NAMES utf8mb4
74 Connect  xyz@localhost on your_db
75 Connect  xyz@localhost on your_db
74 Quit 
75 Prepare  SELECT email FROM customer WHERE email=? LIMIT ?
75 Execute  SELECT email FROM customer WHERE email='a@b.co' LIMIT 5
75 Close stmt   
75 Quit 
73 Quit 

Будь-які нові запити, які створює ваша програма, автоматично відображатимуться у вікні , доки ви продовжуватимете записувати журнал. Щоб вийти з хвоста, натиснітьcmd/ctrl c .

Примітки

  1. Обережно: цей файл журналу може отримати величезну кількість. Я запускаю це лише на своєму сервері розробників.
  2. Файл журналу стає занадто великим? Обрізати це. Це означає, що файл залишається, але вміст видаляється. truncate --size 0 mysql.log.
  3. Класно, що у файлі журналу перелічено з'єднання mysql. Я знаю, що один із них - зі свого застарілого коду mysqli, з якого я переходжу. Третій - з мого нового підключення PDO. Однак не впевнений, звідки береться другий. Якщо ви знаєте швидкий спосіб його знайти, дайте мені знати.

Кредит & спасибі

Величезний крик на відповідь Натана Лонга вище, щоб inspo розібратися в Ubuntu. Також дикірил за коментар до публікації Натана, який привів мене до цього рішення.

Люблю тебе stackoverflow!


0

У середовищі Debian NGINX я зробив наступне.

Перейдіть до /etc/mysql/mysql.conf.dредагування, mysqld.cnfякщо знайдете log-error = /var/log/mysql/error.logнижче два рядки.

general_log_file        = /var/log/mysql/mysql.log
general_log             = 1

Щоб побачити журнали goto /var/log/mysqlі tail -f mysql.log

Не забудьте прокоментувати ці рядки, як тільки ви закінчите налагодження, якщо ви знаходитесь у виробничому середовищі, видаліть, mysql.logоскільки цей файл журналу швидко зросте і може бути величезним.


не всі використовують mysql.
Дрімати крапку з комою
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.