Модуль 1 · Подключение и первые данные

Справочник по MongoDB · модуль 1 · подключение и первые данные

Глава 1.8

1.8

Удаление
и как не удалить лишнего

Последняя из четырёх операций над данными. В этой главе разобраны delete_one и delete_many, разницу между очисткой коллекции и её удалением, приём «мягкого удаления» и порядок действий, который защищает от потери данных.

Категория
Операции над данными
Уровень
Начальный
Связанные темы
Фильтры, обновление, восстановление стенда
Зачем это знать
Удалять ровно то, что задумано, и понимать цену ошибки

Как запускать примеры этой главы

БазыГлава меняет песочницу. Перед началом верните базы в исходное состояние: docker compose run --rm reset или скрипт из главы 1.0, §6.
Архивmongodb-glava-1.8-python.zip, 62 КБmongodb-glava-1.8-ruby.zip, 62 КБmongodb-glava-1.8-go.zip, 69 КБmongodb-glava-1.8-cpp.zip, 66 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.
Файлlessons/ch1_8.py — пример вставляется в конец файлаlessons/ch1_8.rb — пример вставляется в конец файлаlessons/ch1_8.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch1_8.cpp — пример внутрь скобок в конце main, функции — выше main
Запускcd lessons, затем python ch1_8.pycd lessons, затем python3 ch1_8.py · в Docker: docker compose run --rm python lessons/ch1_8.pycd lessons, затем ruby ch1_8.rb · в Docker: docker compose run --rm ruby lessons/ch1_8.rbcd lessons, затем go run ch1_8.go · в Docker: docker compose run --rm go lessons/ch1_8.gocd lessons, затем g++ -std=c++17 ch1_8.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch1_8.cpp
ПорядокКаждый пример вставляется вместо предыдущего и запускается один раз, по порядку, — так получены все выводы главы. Подробно — глава 1.0, §10.

§1delete_one: один документ

Аргумент один — фильтр. Метод удаляет первый подходящий документ и возвращает результат с числом удалённых.

Python · PyMongoудалить по ключу
box = client["sandbox"]["products"]

res = box.delete_one({"_id": "p-020"})
print("удалено:", res.deleted_count)
C++ · mongocxxудалить по ключу
auto box = client["sandbox"]["products"];

auto res = box.delete_one(make_document(kvp("_id", "p-020")));
std::cout << "удалено: " << res->deleted_count() << std::endl;
Go · mongo-driverудалить по ключу
box := client.Database("sandbox").Collection("products")

res, _ := box.DeleteOne(ctx, bson.D{{Key: "_id", Value: "p-020"}})
fmt.Println("удалено:", res.DeletedCount)
Ruby · mongoудалить по ключу
box = client.use("sandbox").database[:products]

res = box.delete_one({ "_id" => "p-020" })
puts "удалено: #{res.deleted_count}"
mongosh · оболочкато же самое
db.products.deleteOne({ _id: "p-020" })
// { acknowledged: true, deletedCount: 1 }
Вывод в терминалрезультат запуска
удалено: 1

Если ничего не нашлось, ошибки не будет — число удалённых окажется нулём. Это тот же принцип, что и у find_one: отсутствие данных не считается сбоем. Поэтому результат нужно проверять: ноль означает, что документ либо уже удалён, либо фильтр написан не так, как задумано.

Python · PyMongoчестный ответ вызывающему
def delete_product(product_id):
    res = box.delete_one({"_id": product_id})
    if res.deleted_count == 0:
        raise LookupError(f"товар {product_id} не найден")

delete_product("p-020")        # p-020 уже удалён предыдущим примером
C++ · mongocxxчестный ответ вызывающему
void delete_product(mongocxx::collection& box, const std::string& product_id) {
    auto res = box.delete_one(make_document(kvp("_id", product_id)));
    if (res->deleted_count() == 0) {
        throw std::runtime_error("товар не найден: " + product_id);
    }
}

// в main: p-020 уже удалён предыдущим примером
delete_product(box, "p-020");
Go · mongo-driverчестный ответ вызывающему
func deleteProduct(ctx context.Context, box *mongo.Collection, productID string) error {
    res, err := box.DeleteOne(ctx, bson.D{{Key: "_id", Value: productID}})
    if err != nil {
        return err
    }
    if res.DeletedCount == 0 {
        return fmt.Errorf("товар %s не найден", productID)
    }
    return nil
}

// в main: p-020 уже удалён предыдущим примером
if err := deleteProduct(ctx, box, "p-020"); err != nil {
    fmt.Println("ошибка:", err)
}
Ruby · mongoчестный ответ вызывающему
def delete_product(box, product_id)
  res = box.delete_one({ "_id" => product_id })
  raise KeyError, "товар #{product_id} не найден" if res.deleted_count.zero?
end

delete_product(box, "p-020")   # p-020 уже удалён предыдущим примером
Вывод в терминалповторное удаление того же товара
LookupError: товар p-020 не найден
Вывод в терминалповторное удаление того же товара
terminate called after throwing an instance of 'std::runtime_error'
what():  товар не найден: p-020
Вывод в терминалповторное удаление того же товара
ошибка: товар p-020 не найден
Вывод в терминалповторное удаление того же товара
товар p-020 не найден (KeyError)

§2delete_many: все подходящие

Тот же фильтр, но удаляются все документы, которые под него подошли.

Python · PyMongoудалить категорию целиком
res = box.delete_many({"category": "аксессуары"})
print("удалено:", res.deleted_count)
print("осталось:", box.count_documents({}))
C++ · mongocxxудалить категорию целиком
auto res = box.delete_many(make_document(kvp("category", "аксессуары")));
std::cout << "удалено: " << res->deleted_count() << std::endl;
std::cout << "осталось: " << box.count_documents(make_document()) << std::endl;
Go · mongo-driverудалить категорию целиком
res, _ := box.DeleteMany(ctx, bson.D{{Key: "category", Value: "аксессуары"}})
fmt.Println("удалено:", res.DeletedCount)

count, _ := box.CountDocuments(ctx, bson.D{})
fmt.Println("осталось:", count)
Ruby · mongoудалить категорию целиком
res = box.delete_many({ "category" => "аксессуары" })
puts "удалено: #{res.deleted_count}"
puts "осталось: #{box.count_documents({})}"
Вывод в терминалрезультат запуска
удалено: 2
осталось: 18
Вывод в терминалрезультат запуска
удалено: 2
осталось: 18
Вывод в терминалрезультат запуска
удалено: 2
осталось: 18
Вывод в терминалрезультат запуска
удалено: 2
осталось: 18

Аксессуаров было три, а удалилось два: один из них, кабель p-020, удалён строкой выше. Отсюда свойство, которое отличает удаление от остальных операций: оно необратимо и меняет данные, на которых построен следующий запрос. Числа, снятые до удаления, после него уже не воспроизвести.

§3Порядок «посчитать → посмотреть → удалить»

Перед удалением тот же фильтр выполняют на чтение. Три шага вместо одного:

Python · PyMongoтри шага вместо одного
query = {"category": "аксессуары", "price": {"$lt": 2000}}

# 1. Сколько документов попадёт под удаление
print("под фильтр попало:", box.count_documents(query))

# 2. Какие именно — глазами, не наугад
for doc in box.find(query, {"_id": 1, "title": 1, "price": 1}).limit(10):
    print("  ", doc)

# 3. И только теперь — удаление тем же фильтром
res = box.delete_many(query)
print("удалено:", res.deleted_count)
C++ · mongocxxтри шага вместо одного
auto query = make_document(kvp("category", "аксессуары"),
    kvp("price", make_document(kvp("$lt", 2000))));

// 1. Сколько документов попадёт под удаление
std::cout << box.count_documents(query.view()) << std::endl;

// 2. Какие именно — глазами, не наугад
for (const auto& doc : box.find(query.view()))
    std::cout << "  " << bsoncxx::to_json(doc) << std::endl;

// 3. И только теперь — удаление тем же фильтром
auto res = box.delete_many(query.view());
std::cout << "удалено: " << res->deleted_count() << std::endl;
Go · mongo-driverтри шага вместо одного
query := bson.D{
    {Key: "category", Value: "аксессуары"},
    {Key: "price", Value: bson.D{{Key: "$lt", Value: 2000}}},
}

// 1. Сколько документов попадёт под удаление
count, _ := box.CountDocuments(ctx, query)
fmt.Println("под фильтр попало:", count)

// 2. Какие именно — глазами, не наугад
cursor, _ := box.Find(ctx, query, options.Find().SetLimit(10))
var rows []bson.M
cursor.All(ctx, &rows)

// 3. И только теперь — удаление тем же фильтром
res, _ := box.DeleteMany(ctx, query)
fmt.Println("удалено:", res.DeletedCount)
Ruby · mongoтри шага вместо одного
query = { "category" => "аксессуары", "price" => { "$lt" => 2000 } }

# 1. Сколько документов попадёт под удаление
puts "под фильтр попало: #{box.count_documents(query)}"

# 2. Какие именно — глазами, не наугад
box.find(query, projection: { "_id" => 1, "title" => 1, "price" => 1 })
   .first(10).each { |doc| puts "  #{doc.inspect}" }

# 3. И только теперь — удаление тем же фильтром
puts "удалено: #{box.delete_many(query).deleted_count}"

Фильтр в трёх местах должен быть одной и той же переменной, а не тремя похожими литералами: расхождение между «посмотрел» и «удалил» и приводит к потерям. Число из первого шага сверяется с deleted_count из третьего — если они разошлись, между шагами данные изменил кто-то ещё.

Осторожно: опечатка в фильтре опаснее, чем в запросе на чтение

Ошибка в имени поля при чтении даёт пустой результат, и она видна сразу. Ошибка в имени поля при удалении даёт пустой фильтр по смыслу и совпадает со всеми документами, если поле в фильтре пропало целиком. Разница между {"categoryy": "аксессуары"} (удалит ноль) и {} (удалит всё) — один символ невнимательности.

§4find_one_and_delete

Иногда удаляемый документ нужен: записать в журнал, положить в архив, вернуть в ответе. Метод find_one_and_delete удаляет и возвращает документ одним действием.

Python · PyMongoудалить и забрать
doc = box.find_one_and_delete({"category": "смартфоны"})
print(doc["_id"], doc["title"], doc["price"])
C++ · mongocxxудалить и забрать
auto doc = box.find_one_and_delete(
    make_document(kvp("category", "смартфоны")));

std::cout << doc->view()["title"].get_string().value << std::endl;
Go · mongo-driverудалить и забрать
var doc bson.M
box.FindOneAndDelete(ctx,
    bson.D{{Key: "category", Value: "смартфоны"}}).Decode(&doc)

fmt.Println(doc["_id"], doc["title"])
Ruby · mongoудалить и забрать
doc = box.find_one_and_delete({ "category" => "смартфоны" })
puts "#{doc['_id']} #{doc['title']}"
Вывод в терминалрезультат запуска
p-006 Смартфон Xiaomi Redmi Note 13 18990
Вывод в терминалрезультат запуска
Смартфон Xiaomi Redmi Note 13
Вывод в терминалрезультат запуска
p-006 Смартфон Xiaomi Redmi Note 13
Вывод в терминалрезультат запуска
p-006 Смартфон Xiaomi Redmi Note 13

Вариант «прочитать, потом удалить» двумя запросами хуже: между ними документ может изменить или удалить другой процесс, и в архив попадёт не тот документ, который удалён. Соображение то же, что и у $inc в прошлой главе: одна операция на сервере надёжнее двух по отдельности.

Рядом стоят find_one_and_update и find_one_and_replace: они возвращают документ до или после изменения (параметр return_document). Типичное применение — очередь задач: забрать одну задачу и одновременно пометить её взятой, чтобы её не взял второй обработчик.

§5Очистить или удалить: delete_many({}) и drop()

Два похожих действия с разными последствиями.

Очистка · коллекция остаётсяPython
res = box.delete_many({})
print("удалено:", res.deleted_count)          # 17
print("коллекция есть:",
      "products" in db.list_collection_names())   # True
Очистка · коллекция остаётсяC++
auto res = box.delete_many(make_document());
std::cout << "удалено: " << res->deleted_count() << std::endl;   // 17

// коллекция осталась: она по-прежнему в списке
for (const auto& name : db.list_collection_names())
    std::cout << name << " ";
Очистка · коллекция остаётсяGo
res, _ := box.DeleteMany(ctx, bson.D{})
fmt.Println("удалено:", res.DeletedCount)        // 17

names, _ := db.ListCollectionNames(ctx, bson.D{})
fmt.Println("коллекции:", names)               // products на месте
Очистка · коллекция остаётсяRuby
res = box.delete_many({})
puts "удалено: #{res.deleted_count}"            # 17
puts "коллекция есть: #{db.collection_names.include?('products')}"
Удаление · коллекции больше нетPython
box.drop()
print("коллекция есть:",
      "products" in db.list_collection_names())   # False
Удаление · коллекции больше нетC++
box.drop();

// в списке коллекций products больше нет
for (const auto& name : db.list_collection_names())
    std::cout << name << " ";
Удаление · коллекции больше нетGo
box.Drop(ctx)

names, _ := db.ListCollectionNames(ctx, bson.D{})
fmt.Println("коллекции:", names)          // products исчезла
Удаление · коллекции больше нетRuby
box.drop

puts "коллекция есть: #{db.collection_names.include?('products')}"  # false
ДействиеДокументыКоллекцияИндексыСкорость
delete_many({})удаляются по одномуостаётсяостаютсязависит от числа документов
drop()исчезают вместе с коллекциейудаляетсяудаляютсямгновенно

Разница проявляется на настроенной коллекции. Если у неё были индексы и правила проверки формы документа, drop() уносит их вместе с коллекцией: новая создастся при первой записи уже без них. Для перезаливки данных курса это неважно, для рабочей базы — существенно.

Базу целиком удаляет client.drop_database("sandbox"). Команда выполняется сразу и необратима: восстанавливать придётся из резервной копии.

§6Мягкое удаление

В рабочих системах документы часто не удаляют вовсе, а помечают удалёнными. Технически при этом выполняется обновление: документ остаётся в коллекции с пометкой.

Python · PyMongoпометить, а не удалить
from datetime import datetime, timezone

box.update_one(
    {"_id": "p-003"},
    {"$set": {"deleted_at": datetime.now(timezone.utc)}})
C++ · mongocxxпометить, а не удалить
auto now = bsoncxx::types::b_date{std::chrono::system_clock::now()};

box.update_one(
    make_document(kvp("_id", "p-003")),
    make_document(kvp("$set",
        make_document(kvp("deleted_at", now)))));
Go · mongo-driverпометить, а не удалить
box.UpdateOne(ctx,
    bson.D{{Key: "_id", Value: "p-003"}},
    bson.D{{Key: "$set", Value: bson.D{
        {Key: "deleted_at", Value: time.Now().UTC()}}}})
Ruby · mongoпометить, а не удалить
box.update_one(
  { "_id" => "p-003" },
  { "$set" => { "deleted_at" => Time.now.utc } }
)
Python · PyMongoвитрина показывает живые
visible = {"deleted_at": {"$exists": False}}

for doc in box.find(visible):
    ...
C++ · mongocxxвитрина показывает живые
auto visible = make_document(kvp("deleted_at",
    make_document(kvp("$exists", false))));

for (const auto& doc : box.find(visible.view())) {
    // ...
}
Go · mongo-driverвитрина показывает живые
visible := bson.D{{Key: "deleted_at",
    Value: bson.D{{Key: "$exists", Value: false}}}}

cursor, _ := box.Find(ctx, visible)
defer cursor.Close(ctx)
Ruby · mongoвитрина показывает живые
visible = { "deleted_at" => { "$exists" => false } }

box.find(visible).each do |doc|
  # ...
end
плюсыДанные восстановимы, история сохранена, отчёты за прошлые периоды не разъезжаются, ошибочное нажатие обратимо.обратимо
минусыПро фильтр «только живые» нужно помнить в каждом запросе. Если его забыть, удалённые документы снова попадут в выдачу.дисциплина

Выбор между настоящим и мягким удалением определяется устройством системы. Заказы, платежи, документы пользователей почти всегда удаляют мягко; черновики, временные записи и кэш — окончательно. Требования закона тоже участвуют: удаление персональных данных по требованию человека должно быть настоящим.

§7Удаление в Compass

В списке документов у каждого есть корзина: наведите курсор — появятся значки правки и удаления. Compass спрашивает подтверждение и показывает, что именно удаляет.

Коллекция sandbox.products в Compass после удаления: 18 документов
Compass · sandbox.products после удаления кабеля и двух аксессуаров: счётчик показывает 1 – 18 of 18 — ровно то число, что вернул count_documents

Удалить документ можно и мышью, по одному:

корзинаЗначок справа вверху документа, рядом с карандашом. Появляется при наведении курсора.шаг 1
подтверждениеCompass подсвечивает документ красным и спрашивает, точно ли удалять. Видно, что именно исчезнет.шаг 2
DELETEУдаление выполняется сразу и необратимо. Счётчик коллекции уменьшается на единицу.шаг 3

Удаление пачкой в Compass делают иначе: задают фильтр в строке поиска, а затем в меню коллекции выбирают нужное действие. Это тот же delete_many, и правило из §3 действует и здесь: сначала смотрят на счётчик под строкой фильтра — он и покажет, сколько документов исчезнет.

Коллекция sandbox.products в Compass после удаления: 18 документов вместо 21
Compass · sandbox.products после двух удалений из §§1–2: вкладка Documents показывает 18, счётчик — 1 – 18 of 18

Кадр снят на песочнице, в которой перед этим выполнены ровно те запросы, что разобраны выше: правка из главы 1.7, затем delete_one и delete_many. Из двадцати одного документа осталось восемнадцать.

§8Восстановление стенда

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

Терминалперезалить базы курса
cd mongodb-practice/stend
bash load.sh

# sandbox.products ← sandbox.products.json
# shop.orders      ← shop.orders.json
# …                                        

То же правило действует и в рабочих системах: данные должны восстанавливаться из репозитория или из резервной копии и не существовать в единственном экземпляре на сервере. Проверка простая — если базу удалить целиком, восстановление должно занимать минуты.

§9Частые ошибки

Что написаноЧто происходитКак правильно
delete_many({}) вместо delete_oneКоллекция пуста. Отменить нельзяПустой фильтр — только осознанно, при перезаливке
delete_one(query) в цикле по спискуМедленно: обращение к серверу на каждый документОдин delete_many({"_id": {"$in": ids}})
Результат не проверяетсяПользователю сказано «удалено», хотя deleted_count равен нулюПроверять deleted_count и отвечать честно
Прочитать, потом удалить двумя запросамиМежду запросами документ мог измениться — в архив уедет не тоfind_one_and_delete
drop() на коллекции с индексамиИндексы и правила проверки исчезли вместе с коллекциейdelete_many({}), если нужно сохранить настройки
Что написаноЧто происходитКак правильно
delete_many(make_document()) вместо delete_oneКоллекция пуста. Отменить нельзяПустой фильтр — только осознанно, при перезаливке
delete_one в цикле по спискуМедленно: обращение к серверу на каждый документОдин delete_many с $in
Результат не проверен на пустотуРазыменование пустого optional вместо честного ответаif (res) res->deleted_count()
Прочитать, потом удалить двумя запросамиМежду запросами документ мог измениться — в архив уедет не тоfind_one_and_delete
drop() на коллекции с индексамиИндексы и правила проверки исчезли вместе с коллекциейdelete_many(make_document()), если нужно сохранить настройки
Что написаноЧто происходитКак правильно
DeleteMany(ctx, bson.D{}) вместо DeleteOneКоллекция пуста. Отменить нельзяПустой фильтр — только осознанно, при перезаливке
DeleteOne в цикле по срезуМедленно: обращение к серверу на каждый документОдин DeleteMany с $in
DeletedCount не проверенПользователю сказано «удалено», хотя не удалилось ничегоПроверять res.DeletedCount и отвечать честно
FindOneAndDelete без проверки ErrNoDocumentsПустая структура принимается за удалённый документСравнить ошибку с mongo.ErrNoDocuments
Drop(ctx) на коллекции с индексамиИндексы и правила проверки исчезли вместе с коллекциейDeleteMany(ctx, bson.D{}), если нужно сохранить настройки
Что написаноЧто происходитКак правильно
delete_many({}) вместо delete_oneКоллекция пуста. Отменить нельзяПустой фильтр — только осознанно, при перезаливке
delete_one в цикле по массивуМедленно: обращение к серверу на каждый документОдин delete_many({ "_id" => { "$in" => ids } })
Результат не проверяетсяПользователю сказано «удалено», хотя deleted_count равен нулюПроверять deleted_count и отвечать честно
find_one_and_delete без проверки на nilNoMethodError при обращении к полямПроверять результат до чтения полей
drop на коллекции с индексамиИндексы и правила проверки исчезли вместе с коллекциейdelete_many({}), если нужно сохранить настройки

§10Проверьте себя

Удаление одного документа вернуло ноль удалённых. Это ошибка?

Нет, это законный ответ: под фильтр ничего не подошло. Ошибки сервер не возвращает, поэтому число удалённых проверяют в коде.

Чем delete_many({}) отличается от drop()?

Первый удаляет документы, оставляя коллекцию с её индексами и правилами. Второй удаляет коллекцию целиком — вместе с индексами и проверкой формы. Второй быстрее, но после него настройки придётся создавать заново.

Почему удалять «прочитал — удалил» двумя запросами хуже, чем find_one_and_delete?

Между двумя запросами документ может изменить или удалить другой процесс. Тогда вы сохраните в архив одно, а удалите другое. Одна операция на сервере такого разрыва не допускает.

Когда мягкое удаление уместнее настоящего?

Когда данные нужны для истории и отчётов или когда ошибочное нажатие должно быть обратимым: заказы, платежи, документы пользователей. Настоящее удаление остаётся для черновиков, временных записей и случаев, когда удалить обязывает закон.

Какой порядок действий защищает от лишнего удаления?

Посчитать count_documents(query), посмотреть find(query) глазами, удалить delete_many(query) — тем же фильтром из одной переменной, а затем сверить deleted_count с числом из первого шага.

§11Шпаргалка

Удалить документ col.delete_one({"_id": "p-020"}) res.deleted_count
Удалить пачку col.delete_many( {"category": "аксессуары"}) col.delete_many({"_id": {"$in": ids}})
Удалить и забрать doc = col.find_one_and_delete( {"_id": "p-008"})
Очистить / снести col.delete_many({}) # пусто col.drop() # нет коллекции client.drop_database("sandbox")
Удалить документ auto res = col.delete_one( make_document(kvp("_id", "p-020"))); res->deleted_count()
Удалить пачку col.delete_many( make_document(kvp("category", "аксессуары")));
Удалить и забрать auto doc = col.find_one_and_delete(filter); // вернётся optional с документом
Очистить / снести col.delete_many(make_document()); col.drop(); client["sandbox"].drop();
Удалить документ res, _ := col.DeleteOne(ctx, bson.D{{Key: "_id", Value: "p-020"}}) res.DeletedCount
Удалить пачку col.DeleteMany(ctx, bson.D{{Key: "category", Value: "аксессуары"}})
Удалить и забрать col.FindOneAndDelete(ctx, filter). Decode(&doc)
Очистить / снести col.DeleteMany(ctx, bson.D{}) col.Drop(ctx) client.Database("sandbox").Drop(ctx)
Удалить документ res = col.delete_one({ "_id" => "p-020" }) res.deleted_count
Удалить пачку col.delete_many( { "category" => "аксессуары" })
Удалить и забрать doc = col.find_one_and_delete(filter)
Очистить / снести col.delete_many({}) # пусто col.drop # нет коллекции client.use("sandbox").database.drop

Итог главы

delete_one убирает первый подходящий документ, delete_many — все; ноль удалённых не считается ошибкой, поэтому результат проверяют. find_one_and_delete удаляет и возвращает документ одной операцией. delete_many({}) оставляет коллекцию с индексами, drop() сносит её целиком. Перед любым удалением фильтр сначала считают и смотрят — это один лишний запрос. На этом четыре базовые операции разобраны: дальше практическая работа, где они соберутся в одну прикладную задачу.

Глава 1.8 · Удаление · Справочник по MongoDB Автор: Макаров Максим Николаевич