Обновление и удаление: UPDATE и DELETE по id — из SQL и из Python
- Писать UPDATE, который обновляет один столбец или несколько столбцов по id
- Объяснить, почему UPDATE без WHERE опасен и что он делает
- Выполнять обновление из Python с параметрами и commit
- Использовать rowcount, чтобы обнаружить, что ни одна строка не обновилась (записи не существует)
- Писать DELETE, который удаляет одну запись по id
- Объяснить, почему DELETE без WHERE — самая опасная команда в SQL
- Удалять из Python с параметрами и commit и распознавать «не найдено» по rowcount
- Понимать, что AUTO_INCREMENT не переиспользует id удалённой записи
В этом уроке
Урок состоит из двух глав — в том же порядке, что и в презентации:
- Глава 1 — UPDATE по id: обновление записи из SQL и из Python
- Глава 2 — DELETE по id: удаление записи из SQL и из Python
Глава 1: UPDATE по id — обновление записи из SQL и из Python
Что мы изучим?
UPDATE = U из CRUD: изменение существующей записи.
- Синтаксис
UPDATE ... SET ... WHERE .... - Обновление одного столбца и нескольких столбцов.
- Почему
WHEREобязателен. - Обновление из Python и
rowcount.
Зачем это нужно?
Каждой системе нужно обновлять: студент сменил email, у товара изменилась цена, заказ перешёл в «отправлен». В API темы API студентов PUT /students/7 = UPDATE students SET ... WHERE id = 7. А ещё UPDATE — команда, которой легче всего уничтожить данные, если забыть одну деталь.
Объяснение с основ
Синтаксис
UPDATE students
SET age = 25
WHERE id = 1;UPDATE students— какая таблица.SET age = 25— какой столбец получает какое новое значение. (Здесь=— присваивание, а внутриWHERE— сравнение. Один знак, две роли.)WHERE id = 1— какую строку (или строки) вы обновляете.
Несколько столбцов — через запятую:
UPDATE students
SET name = 'Dana Cohen', age = 25, email = 'dana.cohen@mail.com'
WHERE id = 1;Значение, вычисленное из существующего значения:
UPDATE products SET stock = stock - 1 WHERE id = 3;«Новый запас = текущий запас минус 1». (Как x = x - 1 в Python.)
WHERE — обязателен!
UPDATE students SET age = 25;Без WHERE изменение применяется ко всем строкам: теперь всем студентам 25. Нет никакого «вы уверены?» и никакого Undo. (После commit старые данные потеряны.)
Привычка профессионалов: прежде чем вручную выполнить UPDATE или DELETE, выполните SELECT с точно таким же WHERE и проверьте, какие строки будут затронуты. Только потом замените SELECT на UPDATE.
UPDATE ... WHERE id = 1 = вы достаёте из шкафа папку номер 1, стираете возраст и пишете новый. UPDATE без WHERE = пройти по всем папкам в шкафу и поменять возраст во всех, потому что вы забыли сказать, какую папку имели в виду.
rowcount — сколько строк изменилось?
После UPDATE из Python у курсора есть cursor.rowcount = число строк, которые действительно обновились:
| rowcount | Значение |
|---|---|
1 | Строка найдена, и её значения изменились ✓ |
0 | Ни одна строка не изменилась: студента с таким id нет — или новые значения совпадают со старыми |
UPDATE для несуществующего id — не ошибка в SQL. Команда «выполнилась» и обновила 0 строк.
Обратите внимание на вторую строку таблицы: в MySQL rowcount считает только строки, которые действительно изменились. Если обновить возраст Ави на 31, а ему уже 31, rowcount будет 0, хотя Ави существует. Поэтому, чтобы узнать, существует ли запись (для 404 у PUT), безопасный способ — получить её (SELECT ... WHERE id = %s) и проверить, не вернулся ли None.
Синтаксис
UPDATE table_name
SET column_a = value_a, column_b = value_b
WHERE id = 7;cursor = connection.cursor()
cursor.execute(
"UPDATE students SET name = %s, age = %s WHERE id = %s",
(new_name, new_age, student_id),
)
connection.commit()
cursor.rowcount # 1 = changed, 0 = nothing changedОбратите внимание на порядок параметров: он следует порядку %s во всём запросе: сначала SET, а в конце — id из WHERE.
Первый пример кода
Таблица до:
id | name | age | email
1 | Dana | 24 | dana@mail.com
2 | Avi | 31 | avi@mail.com
3 | Noa | 22 | noa@mail.comSELECT id, name, age FROM students WHERE id = 2;
UPDATE students SET age = 32 WHERE id = 2;
SELECT id, name, age FROM students WHERE id = 2;Результат:
id | name | age
2 | Avi | 31
id | name | age
2 | Avi | 32Разбор кода построчно
| Запрос | Что произошло |
|---|---|
SELECT ... WHERE id = 2 | До: проверить, что есть и какая строка будет затронута. |
UPDATE students SET age = 32 WHERE id = 2 | Изменилась только строка с id 2. Только age. Имя и email остались. |
SELECT ... WHERE id = 2 | После: проверить. |
Ещё один пример
Обновление из Python, с rowcount и несуществующей строкой. update_demo.py:
import mysql.connector
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
try:
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students")
cursor.execute(
"CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, "
"age INT NOT NULL, email VARCHAR(100) NOT NULL UNIQUE)"
)
cursor.executemany(
"INSERT INTO students (name, age, email) VALUES (%s, %s, %s)",
[("Dana", 24, "dana@mail.com"), ("Avi", 31, "avi@mail.com"), ("Noa", 22, "noa@mail.com")],
)
connection.commit()
cursor.execute(
"UPDATE students SET name = %s, age = %s, email = %s WHERE id = %s",
("Dana Cohen", 25, "dana.cohen@mail.com", 1),
)
connection.commit()
print("Updated rows for id 1:", cursor.rowcount)
cursor.execute("UPDATE students SET age = %s WHERE id = %s", (40, 99))
connection.commit()
print("Updated rows for id 99:", cursor.rowcount)
cursor.execute("UPDATE students SET age = %s WHERE id = %s", (31, 2))
connection.commit()
print("Updated rows for id 2, same age:", cursor.rowcount)
cursor.execute("UPDATE students SET age = age + 1 WHERE age < %s", (30,))
connection.commit()
print("Birthday for everyone under 30:", cursor.rowcount, "rows")
cursor = connection.cursor(dictionary=True)
cursor.execute("SELECT id, name, age, email FROM students ORDER BY id")
for row in cursor.fetchall():
print(row)
finally:
connection.close()Вывод:
Updated rows for id 1: 1
Updated rows for id 99: 0
Updated rows for id 2, same age: 0
Birthday for everyone under 30: 2 rows
{'id': 1, 'name': 'Dana Cohen', 'age': 26, 'email': 'dana.cohen@mail.com'}
{'id': 2, 'name': 'Avi', 'age': 31, 'email': 'avi@mail.com'}
{'id': 3, 'name': 'Noa', 'age': 23, 'email': 'noa@mail.com'}Разбор:
DROP TABLE IF EXISTS— каждый запуск начинается с пустой таблицы (только для демонстрации!).executemany(sql, list_of_tuples)— выполняет один и тот же запрос для каждого tuple в списке. Удобно для вставки нескольких строк.- Обновление 3 столбцов — 4 заполнителя: три для
SETи один дляWHERE, в том же порядке в tuple. - id 99 —
rowcount = 0. Без ошибки. - id 2 с тем же возрастом — тоже
rowcount = 0, хотя Ави существует! Ничего не изменилось. Поэтому «не найдено» проверяют черезSELECT, а не черезrowcount. age = age + 1 WHERE age < 30—WHEREможет выбрать несколько строк. Дане (25, после обновления) и Ноа (22) меньше 30, поэтомуrowcount = 2. Ави не изменился.connection.cursor(dictionary=True)— новый курсор, чтобы в конце получить строки в виде словарей.
Типичная ошибка
Ошибка 1 (главная): UPDATE без WHERE.
cursor.execute("UPDATE students SET email = %s", (new_email,))Все студенты получают один и тот же email. (Здесь благодаря UNIQUE база как раз откажет, потому что два студента не могут получить одинаковый email. Ограничения спасают положение! Но на age не было ограничения, которое бы это остановило.) Проверяйте каждый UPDATE: где WHERE?
Ошибка 2: неправильный порядок параметров.
cursor.execute("UPDATE students SET age = %s WHERE id = %s", (student_id, new_age))Теперь вы обновляете студента, у которого id равен возрасту, на возраст, равный id. Ошибки нет, и это Logical Error, которую очень трудно найти. Порядок в tuple = порядок %s в запросе, слева направо.
Ошибка 3: думать, что UPDATE для несуществующего id выбрасывает ошибку. Нет. Код, который не проверяет, вернёт «успешно обновлено» для несуществующего студента. И наоборот: rowcount == 0 не обязательно значит «не существует» — возможно, просто ни одно значение не изменилось. Проверяйте существование через SELECT.
Ошибка 4: забыть commit (как в INSERT). Обновление видно внутри того же соединения и исчезает после закрытия.
Итоги
UPDATE table SET col = %s, col2 = %s WHERE id = %s— обновление существующей записи. ВSETзнак=— присваивание.- Без
WHEREобновляются все строки. Привычка:SELECTс тем жеWHEREперед ручным запуском. - Новое значение можно вычислять:
stock = stock - 1. - Из Python: параметры в порядке
%s(сначала SET, потом WHERE) иcommit. cursor.rowcount= сколько строк изменилось. 0 = не существует или ни одно значение не изменилось. 404 у PUT решают черезSELECT, который возвращаетNone.
Официальная документация
Глава 2: DELETE по id — удаление записи из SQL и из Python
Что мы изучим?
DELETE = D из CRUD: удаление записей.
- Синтаксис
DELETE FROM ... WHERE .... - DELETE без WHERE — что он делает и как этого избежать.
- Удаление из Python и
rowcount. - Что происходит с id после удаления.
Зачем это нужно?
Пользователь удаляет аккаунт, менеджер удаляет товар, который перестали продавать, студент уходит с курса. В API: DELETE /students/7 = DELETE FROM students WHERE id = 7. А удаление — единственное действие CRUD, у которого нет пути назад: после commit данных больше нет.
Объяснение с основ
Синтаксис
DELETE FROM students WHERE id = 3;DELETE FROM students— из какой таблицы.WHERE id = 3— какие строки удалять.- Списка столбцов нет: удаляют целые строки, а не столбцы. (Чтобы «очистить» поле, используют
UPDATE ... SET field = NULL.)
DELETE без WHERE
DELETE FROM students;Удаляет все строки таблицы. Сама таблица остаётся (пустой), но все данные исчезают. Это, пожалуй, команда, которая «уничтожила» больше всего данных в истории ПО. То же правило, что и в UPDATE: сначала SELECT с тем же WHERE.
rowcount — снова
| rowcount после DELETE | Значение |
|---|---|
1 | Запись удалена ✓ |
0 | Записи с таким id нет. Не ошибка в SQL. В API → 404 |
Что происходит с id?
С AUTO_INCREMENT MySQL не переиспользует id. Если вы удалили студента 3, следующий добавленный студент получит 4 (а не 3). Почему это хорошо? Потому что если бы id 3 перешёл к кому-то другому, всё, что указывало на «студента 3» (ссылка, заказ, лог), вдруг указывало бы на другого человека.
Когда человек умирает, его номер удостоверения не переходит к родившемуся младенцу. Номер, который однажды был занят, навсегда остаётся за той же записью. «Дыры» в нумерации (1, 2, 4, 5) допустимы и даже желательны.
Мягкое удаление (Soft Delete) — на вводном уровне
Во многих системах по-настоящему не удаляют. В таблицу добавляют столбец вроде is_active — 1 = активен, 0 = «удалён», — и «удаление» на самом деле — это UPDATE. Так можно восстановить запись и сохранить историю (например, старые заказы ушедшего клиента остаются действительными).
| Обычное удаление (Hard Delete) | Мягкое удаление (Soft Delete) | |
|---|---|---|
| Команда | DELETE | UPDATE ... SET is_active = 0 |
| Строка | Исчезает из таблицы | Остаётся, только помечена |
| Восстановление | Невозможно | UPDATE ... SET is_active = 1 |
| Чтение | Обычный SELECT | Каждый SELECT списка добавляет WHERE is_active = 1 |
В SQL:
-- the table has: is_active INT NOT NULL DEFAULT 1
-- "delete" = mark the row, do not remove it
UPDATE students SET is_active = 0 WHERE id = 3;
-- every read shows only active students
SELECT id, name FROM students WHERE is_active = 1;
-- 1 | Dana
-- 2 | Avi
-- restore = one more UPDATE
UPDATE students SET is_active = 1 WHERE id = 3;Из Python — точно как delete_student, только с UPDATE вместо DELETE:
def soft_delete_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"UPDATE students SET is_active = 0 "
"WHERE id = %s", (student_id,))
connection.commit()
return cursor.rowcount > 0
finally:
connection.close()DEFAULT 1 означает, что каждый новый студент создаётся активным, и это не нужно писать в INSERT. Цена мягкого удаления: каждый запрос, который показывает список, должен помнить о WHERE is_active = 1, иначе «удалённые» студенты появятся снова. Здесь мы будем использовать настоящий DELETE, но хорошо знать, что такой вариант существует.
Синтаксис
DELETE FROM table_name WHERE id = 7;cursor = connection.cursor()
cursor.execute("DELETE FROM students WHERE id = %s", (student_id,))
connection.commit()
cursor.rowcount # 1 = deleted, 0 = no such rowПервый пример кода
SELECT id, name FROM students WHERE id = 3;
DELETE FROM students WHERE id = 3;
SELECT id, name FROM students;Результат:
id | name
3 | Noa
id | name
1 | Dana
2 | AviРазбор кода построчно
| Запрос | Что произошло |
|---|---|
SELECT ... WHERE id = 3 | Предварительная проверка: что сейчас будет удалено? Ноа. |
DELETE FROM students WHERE id = 3 | Строка Ноа удалена целиком. |
SELECT id, name FROM students | Остались Дана и Ави. |
Ещё один пример
Удаление из Python: rowcount, двойное удаление и id, который не переиспользуется. delete_demo.py:
import mysql.connector
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
try:
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students")
cursor.execute(
"CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL)"
)
cursor.executemany("INSERT INTO students (name) VALUES (%s)", [("Dana",), ("Avi",), ("Noa",)])
connection.commit()
cursor.execute("DELETE FROM students WHERE id = %s", (3,))
connection.commit()
print("Deleted id 3:", cursor.rowcount, "row(s)")
cursor.execute("DELETE FROM students WHERE id = %s", (3,))
connection.commit()
print("Deleted id 3 again:", cursor.rowcount, "row(s)")
cursor.execute("INSERT INTO students (name) VALUES (%s)", ("Tom",))
connection.commit()
print("Tom got id:", cursor.lastrowid)
cursor.execute("SELECT id, name FROM students ORDER BY id")
print("Rows now:", cursor.fetchall())
finally:
connection.close()Вывод:
Deleted id 3: 1 row(s)
Deleted id 3 again: 0 row(s)
Tom got id: 4
Rows now: [(1, 'Dana'), (2, 'Avi'), (4, 'Tom')]Разбор:
DROP TABLE IF EXISTS— каждый запуск начинается с пустой таблицы (только для демонстрации!).[("Dana",), ("Avi",), ("Noa",)]— у каждой строки tuple из одного значения, поэтому после каждого имени стоит запятая.- Первое удаление →
rowcount = 1. - Повторное удаление того же id →
rowcount = 0, без ошибки. В API: первый запрос возвращает 204, а второй — 404. - Том получил 4, а не 3.
AUTO_INCREMENTне переиспользует. В нумерации «дыра», и это нормально.
Типичная ошибка
Ошибка 1: DELETE без WHERE.
cursor.execute("DELETE FROM students")
connection.commit()Все студенты удалены. В каждом куске кода, где есть DELETE: где WHERE и каков его параметр?
Ошибка 2: WHERE, который захватывает больше, чем вы думали.
DELETE FROM students WHERE name = 'Dana';Если Дан две, удалятся обе. Удаляйте по первичному ключу (id), который гарантированно уникален. Имя — не идентификатор.
Ошибка 3: забыть commit. Удаление «работает» внутри соединения и исчезает после закрытия. В случае удаления это скорее «спасительный баг», но всё равно баг.
Ошибка 4: читать запись после того, как вы её удалили. get_student(id) после удаления возвращает None. Если код предполагает, что запись есть (student["name"]), он упадёт. Поэтому delete_student будет возвращать True/False, а не удалённого студента.
Итоги
DELETE FROM table WHERE id = %s— удаляет целые строки. После commit пути назад нет.- Без WHERE удаляются все строки. Удаляйте по первичному ключу и проверяйте заранее через SELECT.
cursor.rowcount: 1 = удалено, 0 = не существовало, без ошибки. В API: 204 / 404.AUTO_INCREMENTне переиспользует id. «Дыры» в нумерации — это нормально.- Мягкое удаление (Soft Delete) — распространённая альтернатива в настоящих системах.
Официальная документация
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: UPDATE в SQL
Напишите команду SQL, которая меняет возраст студента с id 2 на 32. Что произойдёт, если забыть WHERE?
Показать решение
UPDATE students SET age = 32 WHERE id = 2;Без WHERE все студенты в таблице получат возраст 32. WHERE — критическая часть команды: он решает, какие строки обновляются.
Упражнение 2: UPDATE из Python и rowcount
В таблице один студент — Дана (id 1, 24 года). Создайте её так:
import mysql.connector
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students") # start clean, so ids start from 1
cursor.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), age INT)")
cursor.execute("INSERT INTO students (name, age) VALUES ('Dana', 24)")
connection.commit()
connection.close()Напишите update_age(student_id, new_age), которая обновляет с параметрами и возвращает, сколько строк обновилось (rowcount).
print("Updated rows for id 1:", update_age(1, 25))
print("Updated rows for id 99:", update_age(99, 40))Ожидаемый вывод:
Updated rows for id 1: 1
Updated rows for id 99: 0Показать решение
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def update_age(student_id, new_age):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute(
"UPDATE students SET age = %s WHERE id = %s", (new_age, student_id))
connection.commit()
return cursor.rowcount
finally:
connection.close()
setup = get_connection()
cursor = setup.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
"(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), age INT)")
cursor.execute("INSERT INTO students (name, age) VALUES ('Dana', 24)")
setup.commit()
setup.close()
print("Updated rows for id 1:", update_age(1, 25))
print("Updated rows for id 99:", update_age(99, 40))Updated rows for id 1: 1
Updated rows for id 99: 0rowcount равен 0, когда такой строки нет, — это не ошибка. Вызывающий код решает, что с этим делать (например, 404). Обратите внимание: в MySQL rowcount считает только строки, которые действительно изменились, — обновление на точно такое же значение тоже возвращает 0.
Упражнение 3: update_student
Начните снова с таблицы, в которой только Дана (id 1, 24 года), как в упражнении 2. Напишите update_student(student_id, name, age), которая обновляет оба поля и возвращает обновлённого студента в виде словаря или None, если его нет.
print(update_student(1, "Dana Cohen", 26))
print(update_student(99, "Nobody", 1))Ожидаемый вывод:
{'id': 1, 'name': 'Dana Cohen', 'age': 26}
NoneПоказать решение
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def update_student(student_id, name, age):
connection = get_connection()
try:
cursor = connection.cursor(dictionary=True)
cursor.execute(
"UPDATE students SET name = %s, age = %s WHERE id = %s",
(name, age, student_id))
connection.commit()
cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
return cursor.fetchone()
finally:
connection.close()
setup = get_connection()
cursor = setup.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
"(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), age INT)")
cursor.execute("INSERT INTO students (name, age) VALUES ('Dana', 24)")
setup.commit()
setup.close()
print(update_student(1, "Dana Cohen", 26))
print(update_student(99, "Nobody", 1)){'id': 1, 'name': 'Dana Cohen', 'age': 26}
NoneПосле обновления получите строку снова: если такого студента нет, fetchone() возвращает None — по тому же соглашению, что и get_student. (Здесь мы не полагаемся на rowcount, потому что обновление на точно такие же значения возвращает 0, хотя студент существует.)
Упражнение 4: delete_student
В таблице три студента — Дана (1), Ави (2) и Ноа (3). Создайте её так:
import mysql.connector
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students") # start clean, so ids start from 1
cursor.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
cursor.execute("INSERT INTO students (name) VALUES ('Dana'), ('Avi'), ('Noa')")
connection.commit()
connection.close()Напишите delete_student(student_id), которая возвращает True, если строка удалена, и False, если нет. Удалите одного и того же студента (Ноа, id 3) дважды.
print(delete_student(3))
print(delete_student(3))Ожидаемый вывод:
True
FalseПоказать решение
import mysql.connector
def get_connection():
return mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
def delete_student(student_id):
connection = get_connection()
try:
cursor = connection.cursor()
cursor.execute("DELETE FROM students WHERE id = %s", (student_id,))
connection.commit()
return cursor.rowcount > 0
finally:
connection.close()
setup = get_connection()
cursor = setup.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
"(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
cursor.execute("INSERT INTO students (name) VALUES ('Dana'), ('Avi'), ('Noa')")
setup.commit()
setup.close()
print(delete_student(3))
print(delete_student(3))True
FalseПовторное удаление не падает — оно просто не находит строку (rowcount = 0).
Упражнение 5: Что происходит с удалённым id?
С тремя студентами (1, 2, 3): удалите 3, добавьте Тома и напечатайте его id и все строки.
Ожидаемый вывод:
Tom got id: 4
[(1, 'Dana'), (2, 'Avi'), (4, 'Tom')]Показать решение
import mysql.connector
connection = mysql.connector.connect(
host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
"(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
cursor.execute("INSERT INTO students (name) VALUES ('Dana'), ('Avi'), ('Noa')")
connection.commit()
cursor.execute("DELETE FROM students WHERE id = %s", (3,))
cursor.execute("INSERT INTO students (name) VALUES (%s)", ("Tom",))
connection.commit()
print("Tom got id:", cursor.lastrowid)
cursor.execute("SELECT id, name FROM students ORDER BY id")
print(cursor.fetchall())
connection.close()Tom got id: 4
[(1, 'Dana'), (2, 'Avi'), (4, 'Tom')]С AUTO_INCREMENT id не переиспользуется. Это важно: если что-то ещё указывает на id 3 (например, старый заказ), оно не укажет на Тома по ошибке.