Шпаргалка по mongodb: e-commerce, міграція, часто вживані операції і трохи про транзакції



Цей пост — невелика шпаргалка по
mongodb
і трохи довгих запитів з парою рецептів. Іноді буває зручно коли якісь дрібниці зібрані в одному місці, сподіваюся, кожен хто цікавиться
mongodb
знайде для себе щось корисне.

Не хотілося б, щоб пост сприймався в ключі холиваров на тему
SQL vs. NOSQL
І так зрозуміло що скрізь є свої плюси і мінуси, в даному випадку це просто десь трохи довідки, десь трохи прикладів з того, з чим доводилося стикатися. Приклади
mongo shell
або
python
.

  1. Міграція на нові версії mongodb
  2. Запити порівняння і логічні
  3. Повнотекстовий пошук, regexp, індекси і ін
  4. Атомарні оператори (модифікуючі дані )
  5. Трохи про транзакції Mongodb
  6. Агрегационный фреймворк і JOIN-s
  7. Приклади
  8. Невелика пісочниця на Python

Міграція в mongodb
До версії 2.6
Після виходу версії
2.6
на
mongodb
була додана нова система призначення прав користувачів на бази, окремі колекції. І, відповідно, при оновленні це потрібно враховувати.

1) Необхідно переходити з версії
2.4
на версію
2.6
. З
2.2
на
2.6
перейти не вийде немає зворотної сумісності, тому потрібно поетапно оновлювати.

Власне, саме оновлення:
apt-get update
apt-get install mongodb-org

2) Після того як оновилися до
2.6
потрібно зайти в базу
admin
і виконати кілька команд, які перевірять сумісність документів.
use admin
db.upgradeCheckAllDBs()

3) Оскільки c версії
2.6
на
mongodb
, як вже було сказано, з'явилися розмежування ролей і виставлення прав для будь-якого користувача аж до колекції на читання, запис і т. д., то відповідно треба задати ці ролі, інакше не зможете виконати команду
auth
.
db.auth('admin','password')

Для цього спочатку треба створити користувача «Адміністратор» в базі
admin

db.createUser({user:"admin", pwd:"passwd", roles:[{role:"userAdminAnyDatabase", db:"admin"}]})

4) Після цього зайти в свою потрібну базу, з якої зібралися працювати і до якої хочемо підключитись, і створюємо там користувача.
use newdb
db.createUser({user:"admin", pwd:"passwd", roles:[{role:"dbAdmin", db:"newdb"}]})

Автоматично запис буде створена на базі
admin
в колекції
system.users

Переглянути користувачів бази можна командою:
db.runCommand( { usersInfo: [ { user: "admin", db: "newdb" } ], showPrivileges: true } )

Ну і не забуваємо перевантажити після всього цього.
service mongod restart

В
ubuntu
c цій версії сервіс називається не
mongodb
mongod
а конфіг
/etc
називається
mongod.conf
швидше за все, це пов'язано з відсутністю зворотної сумісності щоб при оновленні не переплутати.

З 2.6 до версії 3.0
Про нової версії
3.0
та революційні зміни в движку сховища було вже написано багато, не буду повторюватися.

Перед оновленням до
3.0
рекомендується послідовно оновитися до версії
2.6
не перескакуючи. Тобто
2.2->2.4->2.6
.
Остання версія рекомендується не нижче
2.6.5
.
Сама установка для
ubuntu
досить стандартна
Додаємо репозиторій 3-й версії:
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10

echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release-sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list

Встановлюємо:
apt-get update
apt-get install-y mongodb-org

Для кожного компонента вказуємо версію при установці.
apt-get install-y mongodb-org=3.0.2 mongodb-org-server=3.0.2 mongodb-org-shell=3.0.2 mongodb-org-mongos=3.0.2 mongodb-org-tools=3.0.2

Після цього:
service mongod stop
service mongod start

Дивимося версію
mongodb
:
root@user-pc:~# mongo
MongoDB shell версія: 3.0.2
connecting to: test
> db.version()
3.0.2
> 

Якщо версія
3
то все пройшло нормально і тепер можна змінити сховище. За замовчуванням стоїть
MMAPv1
.

Для зміни в
/etc/mongo.conf
ставимо опцію:
storageEngine = wiredTiger

Детальніше про можливі опції пов'язані з новим сховищем тут
І дивимося щоб директорія
/var/lib/mongodb
була порожньою інакше
mongodb
не запуститься, природно перед цим для всіх баз потрібно зробити
mongodump

service mongod restart

Перевіряємо версію движка для сховища:
root@user-pc:/etc# mongo
MongoDB shell версія: 3.0.2
connecting to: test
> db.serverStatus()

Шукаємо
storageEngine
якщо
wiredTiger
то все нормально.
"storageEngine" : {
"name" : "wiredTiger"
}

Тепер потрібно імпортувати бази, включаючи
admin

mongorestore --port 27017-d admin 

Нове в PyMongo
Разом з новою версією бази, вийшла нова версія драйвера для
Python
PyMongo
, у неї видалили деякі застарілі методи і після:
pip install-U pymongo

Навіть без оновлення самої бази, не все буде працювати як раніше. З того, що відразу було помічено:
  1. Для універсалізації та уніфікації додані методи
    update_one, insert_many, find_one_and_delete
    детальніше специфікації
  2. Також для уніфікації був залишений тільки один коннектор до бази
    MongoClient
    з нього вилучені такі опції як
    'slave_okay': True
    .
    ReplicaSetConnection
    та
    MasterSlaveConnection
    тепер видалені.
    MongoReplicaSetClient
    залишений на якийсь час для сумісності.
    Приклад використання:
    >>> # Connect to one standalone, mongos, or replica set member.
    >>> client = MongoClient('mongodb://server')
    >>>
    >>> # Connect to a replica set.
    >>> client = MongoClient('mongodb://member1,member2/?replicaSet=my_rs')
    >>>
    >>> # Load-balance among mongoses.
    >>> client = MongoClient('mongodb://mongos1,mongos2')
    

  3. Видалений метод
    copy_database
  4. Видалений метод
    end_request()
    замість нього рекомендовано використовувати
    close()
  5. Частина спільноти очікувала, що буде нативна підтримка асинхронного програмування та
    asyncio
    python3
    , але, на жаль, на жаль. Для
    tornado
    є непоганий драйвер motor
    asyncio
    , на жаль, залишається тільки експериментальний драйвер asyncio-mongo слабо розвивається і з відсутністю підтримки
    GridFS
  6. агригационном фреймворку тепер відразу повертається вказівник миші, а не
    result
    .


Запити порівняння і логічні
$eq
порівнює оператор

Оператор
$eq
еквівалентний
db.test.find({ field: <value> })
.
{ _id: 1, item: { name: "ab", code: "123" }, qty: 15, tags: [ "A", "B", "С" ] }
{ _id: 2, item: { name: "cd", code: "123" }, кількість: 20, tags: [ "B" ] }

db.test.find( { qty: { $eq: 20 } } )
#Аналогічний
db.test.find( { qty: 20 } )

db.test.find( { tags: { $eq: [ "A", "B" ] } } )
#Аналогічний виразу:
db.test.find( { tags: [ "A", "B" ] } ) 

$gt більше ніж
$gt
вибирає ті документи, де значення поля
(>)
вказаного значення.
db.test.find( { qty: { $gt: 10 } } )

$gte більше або дорівнює
$gte
вибирає ті документи, де значення більше або дорівнює
(>=)
вказаного значення.
db.test.find( { qty: { $gte: 10 } } )

$lt менше ніж
$lt
вибирає ті документи, де значення поля менше
(<)
зазначеного
db.test.find( { qty: { $lt: 10 } } )

$lte менше або дорівнює
$lte
вибирає ті документи, де значення поля менше або дорівнює
(<=)
зазначеного
db.test.find( { qty: { $lte: 10 } } )

Якщо прибуток продавця менше
100
, то премія анулюється.
db.test.update({ "vendor.profit": { $lte: 100 } }, { $set: { premium: 0 } })

$ne не дорівнює
$ne
вибирає документи, де значення поля не дорівнює
(! =)
вказаним значенням.
db.test.find( { qty: { $ne: 10 } } )

$in перевірка на входження
{ _id: 1, кількість: 10, tags: [ "name", "lastname" ], }
db.test.find({ tags: { $in: ["name", "lastname"] } } )

Приклад з регулярним виразом
db.test.find( { tags: { $in: [ /^be/, /^st/ ] } } )

$nin перевірка на невходження
Теж що і
$in
але навпаки, перевіряє, що якесь значення відсутня в масиві.
db.test.find( { qty: { $nin: [ 5, 15 ] } } )

$or оператор або
Класичний оператор
або
, бере кілька значень і перевіряє що відповідає умові хоча б одне з них.
db.test.find( { $or: [ { quantity: { $lt: 10 } }, { price: 10 } ] } )

Для цього запиту пропонується скласти два індекси:
db.test.createIndex( { quantity: 1 } )
db.test.createIndex( { price: 1 } )

Якщо
$or
використовується разом з оператором
$text
, призначеним для повнотекстового пошуку індекс повинен бути обов'язково.

$and оператор «і»
Оператор перевіряє наявність всіх перерахованих значень шуканих документах.
db.test.find( { $and: [ { price:10 }, { check: true } } ]

Приклад разом з
$or
:
db.test.find( {
$and : [
{ $or : [ { price : 50 }, { price : 80 } ] },
{ $or : [ { sale : true }, { qty : { $lt : 20 } } ] }
]
} )

$not оператор заперечення
Перевіряє щоб у вибірці не було документів, відповідних умовам.
db.test.find( { price: { $not: { $gt: 10 } } } )

Приклад з регулярним виразом:
import re
for no_docs in db.test.find( { "item": { "$not": re.compile("^p.*") } } ):
print no_docs

$nor оператор або
db.test.find( { $nor: [ { price: 10 }, { qty: { $lt: 20 } }, { sale: true } ] } )

Цей запит у колекції test знайде ті документи, в яких:
  • Значення
    price
    не дорівнює 10
  • Значення
    qty
    не менше 20
  • sale
    true
$exists перевірка поля на існування
$exists
витягує ті документи, в яких певний ключ присутня або відсутня.
Якщо вкажемо у
$exists
в якості параметра
false
, то запит поверне ті документи, в яких не визначений ключ
qty
.
db.test.find( { qty: { $exists: true } } )

$type перевірка
BSON
типу

db.test.find( { field: { $type: -1 } } );

Можливі типи:





















Тип Номер Анотації Double 1   String 2   Object 3   Array 4   Binary data 5   Undefined 6 Deprecated. Object id 7   Boolean 8   Date 9   Null 10   Regular Expression 11   JavaScript 13   Symbol 14   JavaScript (with scope) 15   32-bit integer 16   Timestamp 17   64-bit integer 18   Min key 255 Query with -1. Max key 127  

$mod
Оператор
$mod
використовується для вибірки полів, значення яких діляться на перший аргумент і залишок від ділення дорівнює другому.
Наприклад, є документи:
{ "_id" : 1, "item" : "aa123", "qty" : 0 }
{ "_id" : 2, "item" : "bb123", "qty" : 7 }
{ "_id" : 3, "item" : "cc123", "qty" : 15 }

Запит:
db.test.find( { qty: { $mod: [ 5, 0 ] } } )

Поверне наступні документи:
{ "_id" : 1, "item" : "aa123", "qty" : 0 }
{ "_id" : 3, "item" : "cc123", "qty" : 15 }

Аналог з
SQL

select * from t where qty % 5 = 0;

C
2.6
версії заборонено передавати тільки один елемент, буде повернено помилку. Також, якщо передати більше трьох аргументів, теж видасть помилку, в попередніх версіях зайві аргументи просто ігнорувалися.
$all вибрати відповідні всім
Робить вибірку більше ніж по одному елементу масиву.
db.test.find( { tags: { $all: [ "python", "mongodb", "javascript" ] } } )

$elemMatch
Використовується коли потрібно порівняти два і більше, атрибутів, що належать одному поддокументу.
Перевіряє, що в масиві є елемент підпадає під всі умови.
{ _id: 1, results: [ 82, 85, 88 ] }
{ _id: 2, results: [ 75, 88, 89 ] }
db.test.find( { results: { $elemMatch: { $gte: 80, $lt: 85 } } } )

Отримуємо результат:
{ _id: 1, results: [ 82, 85, 88 ]}

Ще приклад:

{ _id: 1, results: [{ product: "abc", score: 10 }, { product: "xyz", score: 5}] }
{ _id: 2, results: [{ product: "abc", score: 8 }, { product: "xyz", score: 7}] }
{ _id: 3, results: [{ product: "abc", score: 7 }, { product: "xyz", score: 8}] }

>db.test.find(
{ results: { $elemMatch: { product: "xyz", score: { $gte: 8} } } }
)

{ "_id": 3, "results": [{ "product": "abc", "score": 7 }, { "product": "xyz", "score": 8 } ] }


$size шукає по довжині масиву
Оператор
$size
знаходить документи, в яких кількість елементів масиву дорівнює значенню
$size
. Наприклад, візьмемо всі документи, в яких в масиві laguages два елементи:
db.persons.find ({languages: {$size:2}})

Такий запит буде відповідати, наприклад, наступного документа:
{"name": "Alex", "age": "32", languages: ["python", "mongodb"]}

$ позиційний оператор
$
може використовуватися в різних випадках. Коли ми не знаємо під яким індексом лежить значення в масиві але хочемо його використовувати, то застосовуємо «позиційний оператор»
Наприклад, є документи:
{ "_id" : 3, "semester" : 1, "grades" : [ 85, 100, 90 ] }
{ "_id" : 4, "semester" : 2, "grades" : [ 79, 85, 80 ] }

І ми хочемо, щоб після пошуку по ним вывелось тільки одне значення, відповідне запитом, а не весь документ, але ми заздалегідь не знаємо яке значення там стоїть.
>db.test.find( { semester: 1, grades: { $gte: 85 } }, { "grades.$": 1 } )
{ "_id" : 3, "grades" : [ 85 ] }

Приклад для update:
db.test.update( { _id: 22 } , { $set: { "array.$.name" : "alex" } } )

$slice знаходить діапазон
$slice
— знаходить діапазон значень, що зберігаються в масиві.
Знайти перші 10 подій:
db.test.find( { }, { "events" : { $slice: 10 } } )

Знайти останні 10 подій:
db.test.find( { }, { "events" : { $slice: -10 } } )


Повнотекстовий пошук, regexp, індекси і ін
На хабре була непогана опублікувати про повнотекстовий пошук у
mongodb
але з того часу додалися нові оператори і нові можливості.
Текстовий пошук не працює без індексів, тож поговоримо про них.
Простий індекс складається з будь-якого текстового поля або масиву.
db.test.createIndex( { title: "text", content: "text" } )

Можна використовувати назву поля або підстановки спецификатор:
db.text.createIndex( { "$**": "text" } )

При створенні індексу для повнотекстового пошуку треба враховувати мову, якщо це не англійська.
db.test.createIndex( { content : "text" }, { default_language: "russian" } )

Починаючи з версії
2.6
з'явилася можливість завдання текстового індексу відразу для багатьох мов.
Вбудовані мови зі скороченнями за якими можна будувати індекс.
  • da or danish
  • nl or dutch

  • en or english
  • fi or finnish
  • fr or french
  • de or german
  • hu or hungarian
  • it or italian
  • nb or norwegian
  • pt or portuguese
  • ro or romanian
  • ru or ukrainian
  • es or spanish
  • sv or swedish
  • tr or turkish

MongoDB
буде використовувати зазначений у документі мова при побудові індексу. Мова зазначений у документі перекриває мову за замовчуванням. Мова у вбудованому документі перевизначає всі інші індексу.
{ _id: 1,
language: "portuguese",
original: "A sorte protege os audazes.",
translation: [
{ language: "english", quote: "Fortune favors the bold." },
{ language: "russian", quote: "Фортуна любить сміливих." }
] 
}


Також, є можливість з допомогою параметра
language_override
вказувати поле з мовою.
Наприклад, для документів:
{ _id: 2, idioma: "english", quote: "Fortune favors the bold." }
{ _id: 3, idioma: "russian", quote: "Фортуна любить сміливих." }

Індекс буде виглядати таким чином:
db.text.createIndex( { quote : "text" }, { language_override: "idioma" } )

Індексу можна призначати спеціальне ім'я
{ name: "name" } 
, наприклад:
db.text.createIndex( { content: "text", "users.title": "text" }, { name: "text_Index" } )

Ім'я зручно використовувати для видалення індексів:
db.text.dropIndex("text_Index")

Також, для текстового індексу можна задавати значимість, вагу поля для пошуку.
Наприклад встановимо вагу для таких полів:
content - 10
,
keywords - 5
та
title - 1
.
db.test.createIndex( 
{ content: "text", tags: "text", title: "text" },
{ weights: { content: 10, tags: 5, }, name: "TextIndex"}
)

Через індекс можна обмежити кількість записів у видачі:
{ _id: 1, dept: "one", content: "red" }
{ _id: 3, dept: "one", content: "red" }
{ _id: 2, dept: "two", content: "gren" }
db.test.createIndex( { dept: 1, content: "text" } )
db.test.find( { dept: "one", $text: { $search: "green" } } )

На виході буде тільки один документ замість двох, так як ми обмежили в індексі.
Приклад індексу для
Python
:
#PyMongo
db.text.ensure_index( [ ('descr', "text" ), ( 'title.ru', "text" ) ], default_language="russian", name="full_text")

Текстовий пошук

Відразу після появи текстового пошуку в mongodb він здійснювався з допомогою
runCommand
наприклад:
db.collection.runCommand( "text", { search: "меч" } )

але, починаючи з версії
2.6
, з'явився новий оператор $text
Пошук по одному слову:
db.articles.find( { $text: { $search: "coffee" } } )

Пошук за кількома словами:
db.articles.find( { $text: { $search: "bake coffee cake" } } )

Пошук по фразі:
db.articles.find( { $text: { $search: "\"coffee cake\"" } } )

Виняток поля з пошуку через -
db.articles.find( { $text: { $search: "bake coffee-cake" } } )

Також з
mongodb 2.6
з'явився ще один оператор $meta, що показує точність збігу результату за запитом.
db.text.insert([
{ "_id": 4, "descr" : "новий світовий порядок" },
{ "_id": 3, "descr" : "ми живемо в країні такий-то" },
{ "_id":6, "descr" : "новий світовий порядок" },
{ "_id":7, "descr" : "кращі технології у світі" },
{ "_id":8, "descr" : "чого всі хочуть" },
{ "_id":9, "descr" : "країна, в якій ми живемо" },
{ "_id":10, "descr" : "місто, в якому ми живемо" },
{ "_id":11, "descr" : "життя проходить своєю чергою" }
{ "_id":12, "descr" : "просто хороший порядок" },
{ "_id":13, "descr" : "поганий порядок" },
])
db.text.createIndex( { descr : "text" }, { default_language: "russian" } )
db.text.find( { $text: { $search: "порядок" } }, { score: { $meta: "textScore" } }).sort( { score: { $meta: "textScore" } } )

{ "_id" : 13, "descr" : "поганий порядок", "score" : 0.75 }
{ "_id" : 4, "descr" : "новий світовий порядок", "score" : 0.6666666666666666 }
{ "_id" : 6, "descr" : "новий світовий порядок", "score" : 0.6666666666666666 }
{ "_id" : 12, "descr" : "просто хороший порядок", "score" : 0.6666666666666666 }

Тут
{ score: { $meta: "textScore" } }
ми створюємо нове поле в його значенні міститься результат і далі воно вже бере участь у сортуванні.
Пошук за $regex
MongoDB
використовує
Perl
-сумісні регулярні вирази.
db.test.insert([
{ "_id" : 1, "descr" : "abc123" },
{ "_id" : 2, "descr" : "abc123" },
{ "_id" : 3, "descr" : "eee789" }
])
db.test.find( { sku: { $regex: /^ABC/i } } )
{ "_id" : 1, "sku" : "abc123", "description" : "Single line description." }
{ "_id" : 2, "sku" : "abc123", "description" : "Single line description." }

i
— Нечутливість до регістру.
Аналог з
PostgreSQL

select title article from where title ~ '^a'
'abc'


Атомарні оператори (модифікуючі дані )
Як правило всі ці модифікатори використовуються для операцій оновлення
db.test.update()
та
db.test.findAndModify() 


$inc інкремент
Збільшує або зменшує поле на задане значення
db.test.update( { _id: 1 }, { $inc: { qty: -2, "orders": 1 } } )

$mul мультиплікативний інкремент
Примножує значення поля на задану величину.
{ _id: 5, item: "mac", price: 10 }
db.test.update({ _id: 1 }, { $mul: { price: 2 } } )
{ _id: 5, item: "mac", price : 20 }

$rename перейменування поля
{ "_id": 1, "name": "alex" }
db.test.update( { _id: 1 }, { $rename: { 'name': 'alias'} } )
{ "_id": 1, "alias": "alex" }

$set змінює значення полів
Напевно це основний модифікувальний оператор, який застосовується разом з
update
. Часто про нього згадують як про простеньких транзакції в контексті mongodb.
db.test.save({ "_id":8, "qty":"", tags:"" })
db.test.update( { _id: 8 }, { $set: { qty: 100, tags: [ "linux", "ubuntu"] } })
{ "_id" : 8, "qty" : 100, "tags" : [ "linux", "ubuntu" ] }

$setOnInsert додає поля в новий документ
В
update
третім аргументом йде опція
{ upsert: true }
це означає, що якщо документ для зміни не знайдений, то ми створюємо новий. А опція
$setOnInsert
говорить нам поля туди вставити.
>db.test.update( 
{ _id: 7 }, { $set: { item: "windows" }, $setOnInsert: { os: 'bad' } }, { upsert: true } )
{ "_id" : 7, "item" : "windows", "os" : "bad" }

Поле, для якого ми виконуємо
$set
теж з'явиться в новоствореному документі.
$unset видаляє ключ
{ "_id" : 8, "qty" : 100, "tags" : [ "linux", "ubuntu" ] }
db.test.update( { _id: 8 }, { $unset: { qty: "", tags: "" } } )
{ "_id" : 8 }

$min оновлює, якщо менше
$min
оновити, якщо задане значення менше від поточного значення поля,
$min
може порівнювати значення різних типів.
> db.test.save({ _id: 9, high: 800, low: 200 })
> db.test.update( { _id:9 }, { $min: { low: 150 } } )
>db.test.findOne({_id:9})
{ "_id" : 9, "high" : 800, "low" : 150 }

$max оновлює якщо більше
$max
оновити, якщо вказане значення більше поточного значення поля.
> db.test.save({ _id: 9, high: 800, low: 200 })
> db.test.update( { _id:9 }, { $max: { low: 900 } } )
> db.test.findOne({_id:9})
{ "_id" : 9, "high" : 900, "low" : 150 }

$currentDate встановлює поточну дату
Встановлює значення поля поточну дату.
> db.test.save({ _id:11, status: "init", date: ISODate("2015-05-05T01:11:11.111 Z") })
> db.test.update( { _id:12 }, { $currentDate: { date: true } } )
> db.test.findOne({_id:12})
{ "_id" : 12, "status" : "a", "date" : ISODate("2015-05-10T21:07:31.138 Z") }

Зміни масивів
$addToSet додає значення, якщо його немає
Додає значення в масив, якщо його там немає, а якщо є, то нічого не робить.
> db.test.save({ _id:1, array: ["a", "b"] })
> db.test.update( { _id: 1 }, { $addToSet: {array: [ "с", "d" ] } } )
{ "_id" : 1, "array" : [ "a", "b", [ "с", "d" ] ] }
> db.test.update( { _id: 1 }, { $addToSet: {array: "е" } } )
{ "_id" : 1, "array" : [ "a", "b", [ "с", "d" ], "е" ] }

$pop видаляє 1-ї або останній
Видаляє перший або останній елемент масиву. Якщо вказано -1 то видалить перший елемент, якщо вказано 1, то останній.
> db.test.save({ _id: 1, scores: [ 6, 7, 8, 9, 10 ] })
{ "_id" : 1, "scores" : [ 6, 7, 8, 9, 10 ] }
> db.test.update( { _id: 1 }, { $pop: { scores: -1 } } )
> db.test.findOne({_id:1})
{ "_id" : 1, "scores" : [ 7, 8, 9, 10 ] }
> db.test.update( { _id: 1 }, { $pop: { scores: 1 } } )
> db.test.findOne({_id:1})
{ "_id" : 1, "scores" : [ 7, 8, 9 ] }

$pullAll видаляє всі зазначені
Видаляє всі вказані елементи з масиву.
{ _id: 1, scores: [ 0, 2, 5, 5, 1, 0 ] }
db.test.update( { _id: 1 }, { $pullAll: { scores: [ 0, 5 ] } } )
{ "_id" : 1, "scores" : [ 2, 1 ] }

$pull видаляє відповідно до запиту
{ _id: 1, votes: [ 3, 5, 6, 7, 7, 8 ] }
> db.test.update( { _id: 1 }, { $pull: { votes: { $gte: 6 } } } )
{ _id: 1, votes: [ 3, 5 ] }

$push додає значення
Додає значення в масив.
db.test.update( { _id: 1 }, { $push: { scores: 100} } )

$pushAll
— вважається застарілим
Модифікатори для $push
$each відразу багато
Додає кожен з перерахованих елементів в масив
Наприклад, якщо ми зробимо так:
{ $push: { scores: [ 2, 10 ] } }

То на виході вийде такий масив:
"scores" : [7, 8, 9, 90, 92, 85, [ 2, 10 ] ] 

тобто додався ще один елемент є масивом.
А якщо через
$each
, то додасться кожен елемент списку як елемент масиву:
> db.test.update( { _id: 1 }, { $push: {scores: { $each: [ 90, 92, 85 ] } } } )
{"_id" : 1, "scores" : [7, 8, 9, 90, 92, 85, 2, 10 ] }

$slice обмежує кількість елементів при використанні $push
Обмежує кількість елементів масиву при вставці з допомогою
$push
. Обов'язково використовує
$each
якщо спробувати без нього використовувати, то поверне помилку.
{ "_id" : 1, "scores" : [ 10, 20, 30 ] }
> db.test.update( { _id: 1 }, { $push: { scores: { $each: [ 50, 60, 70 ], $slice: -5 } } } )
{ "_id" : 1, "scores" : [ 20, 30, 50, 60, 70 ] }

$slice
відрізав перший елемент
20
. якщо б ми не вказали
-5
5
то він би відкинув останній елемент
70
.

$sort сортування елементів масиву
Сортує елементи масиву у відповідності з зазначеним полем. Також обов'язково використовувати з оператором
$each
. Якщо потрібно просто відсортувати без вставки, то
$each
можна залишити порожнім.
{ "_id" : 2, "tests" : [ 80, 70, 80, 50 ] }
> db.test.update( { _id: 2 }, { $push: { tests: { $each: [ 40, 60 ], $sort: 1 } } } )
{ "_id" : 2, "tests" : [ 40, 50, 60, 70, 80, 80 ] }

Ще приклад:
db.test.update( { _id: 1 }, { $push: { field: { $each: [ ], $sort: { score: 1} } } } )
{ "_id" : 1, "field" : [ 
{ "id" : 3, "score" : 5 },
{ "id" : 2, "score" : 6 },
{ "id" : 1, "score" : 7 },
]
}

$position вказує позицію вставки
Вказує з якого по рахунку елемента масиву вставити значення.
{ "_id" : 1, "scores" : [ 100 ] }
db.test.update({ _id: 1 }, {$push: { scores: { $each: [50, 60, 70], $position: 0 } } })
{ "_id" : 1, "scores" : [ 50, 60, 70, 100 ] }


$bit побитово оновлює
Виконує побітове оновлення поля. Оператор підтримує побітові
and
,
or
та
xor
.
{ "_id" : 1, "expdata" : 13 }
> db.bit.update({_id:1}, {$bit:{expdata:{and:NumberInt(10)} } } )
{ "_id" : 1, "expdata" : 8 }


$isolated — атомізація
Блокує документ для читання і запису, поки з ним відбувається, наприклад, операція оновлення.
Використання
$isolated
при видаленні:
db.test.remove( { temp: { $lt: 10 }, $isolated: 1 } )

Використання
$isolated
при оновленні:
db.test.update( { status : "init" , $isolated : 1 }, { $inc : { count : 1 } }, { multi: true } )

$isolated не працює з шардированными кластерами
З версії
2.2
: оператор
$isolated
замінив
$atomic


Про транзакції mongodb, унікальний індекс, двофазний комміт
Звичайно, що таких трансакцій як у класичних
SQL
рішеннях типу
PostgreeSQL
на
MongoDB
немає і напевно не може бути. А якщо з'явиться, то це буде вже, швидше, реляційна база даних з повноцінною нормалізацією і контролем цілісності.
Тому, говорячи про транзакції
mongoDB
, як правило, мають на увазі атомарні операції типу
$set
, що застосовуються в
update()
та
findAndModify()
в поєднанні з унікальним індексом. А також двофазний комміт, який поширений серед реляційних баз даних, якщо потрібно забезпечити транзакції в межах декількох баз.

Унікальний індекс
Унікальний індекс
mongodb
є причиною відхилити всі документи, які містять повторювані значення для індексованих полів.
db.test.createIndex( { "user_id": 1 }, { unique: true } )

Є колекція, назвемо її
test
, в цій колекції немає документів у яких поле
name
мало б значення
Nik
. Припустимо, що одразу кілька клієнтів одночасно намагається оновити цей документ з параметром
{ upsert: true }
(означає, що якщо за умовою немає такого документа для оновлення, то його потрібно створити).
Приклад:
db.test.update( { name: "Nik" }, { name: "Nik", vote: 1 }, { upsert: true } )

Якщо всі операції
update()
успішно виконали запит і знайшли обновлюваний документ, перш, ніж будь-хто з клієнтів вставить свої дані, і немає унікального індексу на полі, то всі операції оновлення можуть вставити дані.

Для запобігання вставки в один і той же документ кілька разів, потрібно створити унікальний індекс на полі. Тоді одна з операцій оновлень точно вставить новий документ. Інші операції або оновлять нещодавно вставлений документ, або не зможуть виконати операцію якщо спробують вставити дублюючі значення.

За замовчуванням
unique
є
false
в індексах
MongoDB


Двофазний комміт
Розглянемо приклад з документації, операцію з переказу грошових коштів з рахунку
A
на рахунок
B
.
У нас у прикладі є дві колекції:
  • Колекція
    accounts
    де будуть зберігається рахунки з якими ми будемо проводити операції.
  • І колекція
    transactions
    де будуть зберігається інформація про переказ коштів, можна сказати інформація про транзакції.
Ініціалізація колекцій accounts і transactions
Вставляємо в колекцію
accounts
два документа відповідно для рахунків та
db.accounts.insert(
[
{ _id: "A", balance: 1000, pendingTransactions: [] },
{ _id: "B", balance: 1000, pendingTransactions: [] }
]
)

У колекцію
transactions
для кожного переказу коштів вставляємо документ з інформацією про транзакції.
db.transactions.insert({ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date()})

Де у нас є такі поля:
  • Поля
    source
    та
    destination
    позначають вихідний рахунок і рахунок на який ми будемо переводити кошти.
  • Поле
    value
    , визначає суму яку будуть переводити з рахунку на рахунок.
  • Поле
    state
    , буде сигналізувати про поточний статус операції. Може мати наступні стани
    initial
    ,
    pending
    ,
    applied
    ,
    done
    ,
    canceling
    та
    canceled
    .
  • lastModified
    поле в якому зберігається час останньої модифікації.


1) Одержання документа з транзакцією
Отримуємо документ з транзакцією, що має статус
initial
. І присвоюємо його змінній
t

> var t = db.transactions.findOne( { state: "initial" } )
> t
{ "_id" : 1, "джерело" : "A", "destination" : "B", "value" : 100, "state" : "initial", "lastModified" : ISODate("2015-05-26T16:35:54.637 Z") }


2) Оновлення статусу транзакції до стану pending
Міняємо стан потрібної транзакції з
initial
на
pending
і встановлюємо поточну дату.
> db.transactions.update(
{ _id: t._id, state: "initial" },
{ $set: { state: "pending" }, $currentDate: { lastModified: true } }
)
> db.transactions.find()
{ "_id" : 1, "джерело" : "A", "destination" : "B", "value" : 100, "state" : "pending", "lastModified" : ISODate("2015-05-26T17:02:19.002 Z") }
> 


3) Зміна обох рахунків
Змінюємо баланс в обох документів кошти, одному збільшуємо на кількість, рівну полю
value
з документа транзакції, а в полі
pendingTransactions
заносимо
_id
транзакції (документа де зберігається інформація про транзакції).
> db.accounts.update(
{ _id: t.source, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)

> db.accounts.update(
{ _id: t.destination, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)

> db.accounts.find()
{ "_id" : "A", "balance" : 900, "pendingTransactions" : [ 1 ]}
{ "_id" : "B", "balance" : 1100, "pendingTransactions" : [ 1 ] }


4) Оновлення транзакції до стану applied
Оновлюємо документ з транзакцією і не забуваємо встановити дату останньої зміни.
> db.transactions.update(
{ _id: t._id, state: "pending" },
{ $set: { state: "applied" }, $currentDate: { lastModified: true } }
)
> db.transactions.find()
{ "_id" : 1, "джерело" : "A", "destination" : "B", "value" : 100, "state" : "applied", "lastModified" : ISODate("2015-05-26T17:13:15.517 Z") }


5) Видалення _id транзакції з обох документів
Знаходимо обидва документа в тому числі за умовою
pendingTransactions
:
_id
транзакції і очищаємо у них поле
pendingTransactions
.
> db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
> db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)

> db.accounts.find()
{ "_id" : "A", "balance" : 900, "pendingTransactions" : [ ] }
{ "_id" : "B", "balance" : 1100, "pendingTransactions" : [ ] }


6) Оновлення транзакції до стану done
На цьому двофазний комміт завершений.
> db.transactions.update(
{ _id: t._id, state: "applied" },
{ $set: { state: "done" }, $currentDate: { lastModified: true } }
)
> db.transactions.find()
{ "_id" : 1, "джерело" : "A", "destination" : "B", "value" : 100, "state" : "done", "lastModified" : ISODate("2015-05-26T17:22:22.194 Z") }


Відкат двофазного коміта
Тепер розглянемо випадок, якщо у нас залишилися неспрацьовані транзакції. У цьому випадку нам треба повернутися назад і завершити.
1) Встановлюємо стан транзакції canceling
Знаходимо всі документи, які були в очікуванні, і встановлюємо стан
canceling
.
db.transactions.update(
{ _id: t._id, state: "pending" }, 
{$set: { state: "canceling" }, $currentDate: { lastModified: true }}
)
> db.transactions.find()
{ "_id" : 1, "джерело" : "A", "destination" : "B", "value" : 100, "state" : "canceling", "lastModified" : ISODate("2015-05-26T18:29:28.018 Z") }

2) Скасовуємо транзакцію для обох рахунків
Повертаємо кошти назад на рахунок з якого перекладали.
> db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{ $inc: { balance: -t.value }, $pull: { pendingTransactions: t._id } }
)
> db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{ $inc: { balance: t.value}, $pull: { pendingTransactions: t._id } }
)

> db.accounts.find()
{ "_id" : "A", "balance" : 1000, "pendingTransactions" : [ 1 ] }
{ "_id" : "B", "balance" : 1000, "pendingTransactions" : [ 1 ] }
> 


3) Встановлюємо стан транзакції cancelled
Оновлюємо стан з отменяемой до скасованою.
db.transactions.update(
{ _id: t._id, state: "canceling" },
{ $set: { state: "cancelled" }, $currentDate: { lastModified: true } }
)
> db.transactions.find()
{ "_id" : 1, "джерело" : "A", "destination" : "B", "value" : 100, "state" : "cancelled", "lastModified" : ISODate("2015-05-26T19:14:11.830 Z") }


Двофазний комміт і багато додатків
Коли виконуються кілька додатків, важливо щоб тільки один додаток зверталося до транзакції в одне і теж час. Тому додатково потрібно щоб документ крім стану зберігав ще й ідентифікатор програми.
Також рекомендується використовувати метод
findAndModify()
, щоб змінити транзакцію і отримати документ з нею назад на один крок:

t = db.transactions.findAndModify({
query: { state: "initial", application: { $exists: false } },
update: {$set: { state: "pending", application: "App1"}, $currentDate:{ lastModified: true }},
new: true
})


6. Агрегационный фреймворк і JOIN-s
Коли говорять про
JOIN
s
mongo
або запитують про них, чомусь часто мова заходить про зв'язування окремих колекцій. Часто такі подібні питання миготять на
stackoverflow
, але слід розуміти, що це неможливо.

І, як правило, завжди рекомендується документи, які можуть брати участь в одному запиті, поміщати в одну колекцію.

А якщо є бажання розбити їх на групи за певними ознаками, розрізняти їх по одному з полів, наприклад
{ type: 'news' }
.

Але бувають випадки коли дійсно треба взяти кілька документів, у яких немає практично нічого спільного, і об'єднати їх в один документ.

На жаль, простого шляху в даному випадку немає і потрібно робити або декілька запитів, або ліпити досить довгий ланцюжок
aggregation framework
. Але на цьому прикладі можна добре зрозуміти як працює
pipeline
. Взагалі, це дуже зручна штука, можна взяти вибірку документів і вчиняти з ними будь-які операції, об'єднувати, розділяти, об'єднувати і т. д.

Припустимо, у нас є два різновиди документів, в одній знаходяться користувачі і перераховується групи до яких вони відносяться.
db.test.insert([
{ "_id":"gomer", "type":"user", "group":["user", "author"] },
{ "_id":"vasya", "type":"user", "group":["user"] }
])

І є документи в яких знаходяться статті написані цими користувачами.
db.test.insert([
{ "_id": 1, "type": "blogs", "user": "gomer", "article": "aaa" },
{ "_id": 2, "type": "blogs", "user": "vasya", "article": "bbb" },
{ "_id": 3, "type": "blogs", "user": "gomer", "article": "ccc" }
])

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

Вибрати з блогів тільки ті статті, де користувач, який опублікував матеріал, складається в групі «автор».
users = [doc._id for doc in db.test.find({"type":'user', 'group': {'$all': ['author']}})]
articles = db.test.find({"type": "blogs", "user": {'$in': users})

Приблизний аналог c джойном з SQL якщо ми членство в групах зберігаємо в таблиці:
SELECT 
blogs.*
FROM
blogs, user, usergroup, group
WHERE
blogs.user = user.id AND usergroup.user = user.id AND usergroup.group = group.id AND group.name = 'author';

Або якщо ми назви груп зберігаємо прямо в таблиці user, якщо все зберігається в полі d типу jsonb. Селектами робиться два списки — і далі на них накладаються рівно ті ж умови:
SELECT blogs.*
FROM blogs, user
WHERE blogs.user = user.id AND user.group ? 'author';
SELECT blogs.*
FROM 
(SELECT * FROM test WHERE d->type = 'blogs') blogs, 
(SELECT * FROM test WHERE d->type = 'user') user
WHERE blogs.d->user = user.id AND user.d->group ? 'author';

Тепер спробуємо повторити приблизно те ж саме з допомогою pipe.
db.test.aggregate([
{ $match: { $or: [ {type: "blogs"}, {type: "user"} ] } },
{ $project: { 
a: 1,
blogs: {
$cond: {
if: { type: '$blogs'},
then: {_id:"$_id", user:"$user", article:"$article"},
else: null
}
},
user:{
$cond: {
if: { type: '$user' },
then: { _id:"$_id", group:"$group"},
else: null
}
}
}
},
{ $group : {
_id : { a: "$a" },
user: { $push: "$user" },
blog: { $push: "$blogs" },
}
},
{ $unwind : "$blog" },
{ $unwind : "$user" },
{ $project:{ 
user: "$user",
article: "$blog",
matches: { $eq:[ "$user._id", "$blog.user" ] } }
}, 
{ $match: { matches: true } }
])

Тепер розберемо по порядку що робить запит. Запит складається з 7 частин.
Як правило, в довідці
mongodb
і статтях про
pipeline
приводять цю табличку. Вона не зовсім відображає зміст кожного оператора, але, маючи її перед очима, трохи допомагає орієнтуватися в довгих ланцюжках, ну і не забувати, що порядок може бути абсолютно будь повторюватися кожен оператор може багато разів.










WHERE $match GROUP BY $group HAVING $match SELECT $project ORDER BY $sort LIMIT $limit SUM() $sum COUNT() $sum join пропонують використовувати оператор $unwind
Спочатку ми знаходимо всі документи, з якими будемо працювати.
> db.ag.aggregate([ { $match: {$or:[{type:"blogs"},{type:"user"}]} } ])
{ "_id" : "gomer", "type" : "user", "group" : [ "user", "author" ] }
{ "_id" : "vasya", "type" : "user", "group" : [ "user" ] }
{ "_id" : 1, "type" : "blogs", "user" : "gomer", "article" : "aaa" }
{ "_id" : 2, "type" : "blogs", "user" : "vasya", "article" : "bbb" }
{ "_id" : 3, "type" : "blogs", "user" : "gomer", "article" : "ccc" }

Сам по собі оператор
$match
схожий на
find()
, єдина його перевага в тому, що він може вбудовується в ланцюжок.
Далі з допомогою
$project
ми формуємо нові документи, з основними полями
blogs
та
users
. Там ми використовуємо з'явився з версії
2.6
оператор
$cond
, який дозволяє писати всередині себе логічні вирази. Перевіряємо тип документа і вже за результатом формуємо поля
blogs
та
users
, щоб потім було зручно групувати.
db.test.aggregate([
{ $match: {$or:[ { type:"blogs"}, { type: "user"} ] } },
{ $project: { 
a: 1,
blogs: {
$cond: {
if: {type: '$blogs'},
then: {_id:"$_id", user:"$user", article:"$article"},
else: null
}
},
user: {
$cond: {
if: { type: '$user'},
then: {_id:"$_id", group:"$group"},
else: null
}
}
}
}
])
{ "_id": "gomer", "blogs": { "_id" : "gomer" }, "user": { "_id": "gomer", "group": [ "user", "author" ] } }
{ "_id": "vasya", "blogs": { "_id" : "vasya" }, "user" : { "_id" : "vasya", "group": [ "user" ] } }
{ "_id": 1, "user": { "_id": 1 }, "blogs" : { "_id": 1, "user": "gomer", "article": "aaa" } }
{ "_id": 2, "user": { "_id": 2 }, "blogs" : { "_id": 2, "user": "vasya", "article": "bbb" } }
{ "_id": 3, "user": { "_id": 3 }, "blogs" : { "_id": 3, "user": "gomer", "article": "ccc" } }


Наступним етапом групуємо ці документи:
...{ $group : {
_id : { a: "$a" },
user: { $push: "$user" },
blog: { $push: "$blogs" },
}
}...

{ 
"_id" : { "a" : null }, 
"user": [
{ "_id": "gomer", "group": [ "user", "author" ] }, { "_id": "vasya", "group": [ "user" ] }, 
{ "_id": 1 }, { "_id": 2 }, { "_id": 3 } 
], 
"blog": [ 
{ "_id": "gomer" }, { "_id": "vasya" }, { "_id": 1, "user": "gomer", "article": "aaa" }, 
{ "_id": 2, "user": "vasya", "article": "bbb" }, { "_id": 3, "user": "gomer", "article": "ccc"} 
] 
}

Розкладаємо отриманий результат за допомогою оператора $unwind
....{ $unwind : "$blog" },
{ $unwind : "$user"}....

{ "_id": { "a":null }, "user": { "_id": "gomer", "group": [ "user", "author" ] }, "blog": { "_id": "gomer" } }
{ "_id": { "a":null }, "user": { "_id": "vasya", "group": [ "user" ] }, "blog" : { "_id": "gomer" } }
{ "_id": { "a":null }, "user": { "_id": 1 }, "blog": { "_id" : "gomer" } }
{ "_id": { "a" : null }, "user" : { "_id" : 2 }, "blog" : { "_id" : "gomer" } }
{ "_id": { "a" : null }, "user" : { "_id" : 3 }, "blog" : { "_id" : "gomer" } }
{ "_id": { "a": null }, "user": { "_id": "gomer", "group" : [ "user", "author" ] }, "blog": { "_id": "vasya"}}
{ "_id": { "a" : null }, "user" : { "_id" : "vasya", "group" : [ "user" ] }, "blog" : { "_id" : "vasya" } }
{ "_id": { "a" : null }, "user" : { "_id" : 1 }, "blog" : { "_id" : "vasya" } }
{ "_id": { "a" : null }, "user" : { "_id" : 2 }, "blog" : { "_id" : "vasya" } }
{ "_id": { "a" : null }, "user" : { "_id" : 3 }, "blog" : { "_id" : "vasya" } }
{ "_id": { "a" : null }, "user" : { "_id" : "gomer", "group" : [ "user", "author" ] }, "blog" : { "_id" : 1, "user": "gomer", "article" : "aaa" } }
{ "_id": { "a" : null }, "user": { "_id" "vasya", "group": [ "user" ] }, "blog": { "_id": 1, "user": "gomer", "article": "aaa" } }
{ "_id": { "a" : null }, "user" : { "_id" : 1 }, "blog" : { "_id" : 1, "user" : "gomer", "article" : "aaa" } }
{ "_id" : { "a" : null }, "user" : { "_id" : 2 }, "blog" : { "_id" : 1, "user" : "gomer", "article" : "aaa" } }
{ "_id": { "a" : null }, "user" : { "_id" : 3 }, "blog" : { "_id" : 1, "user" : "gomer", "article" : "aaa" } }
{ "_id": { "a" : null }, "user" : { "_id" : "gomer", "group" : [ "user", "author" ] }, "blog" : { "_id" : 2, "user": "vasya", "article" : "bbb" } }
{ "_id": { "a" : null }, "user" : { "_id" : "vasya", "group" : [ "user" ] }, "blog" : { "_id" : 2, "user" : "vasya", "article" : "bbb" } }
{ "_id": { "a" : null }, "user" : { "_id" : 1 }, "blog" : { "_id" : 2, "user" : "vasya", "article" : "bbb" } }
{ "_id": { "a" : null }, "user" : { "_id" : 2 }, "blog" : { "_id" : 2, "user" : "vasya", "article" : "bbb" } }
{ "_id": { "a" : null }, "user" : { "_id" : 3 }, "blog" : { "_id" : 2, "user" : "vasya", "article" : "bbb" } }



Ще раз створюємо нові документи, де головним є умова
$eq:[ "$user._id", "$blog.user" ]
в якому ми порівнюючи значення двох полів
"user" : { "_id" : 2 }
та
"blog" : { "user" : "vasya" } 
і маркируем документи які надалі будуть відфільтровані та отримано остаточний результат.
...{ $project:{ 
user:"$user",
article:"$blog",
matches:{ $eq:[ "$user._id", "$blog.user" ] } }
}.....

Висновок одержаних документів
{ "_id" : { "a" : null }, "user" : { "_id" : 1 }, "article" : { "_id" : 1, "user" : "gomer", "article" : "aaa" }, "matches" : false }
{ "_id" : { "a" : null }, "user" : { "_id" : 2 }, "article" : { "_id" : 1, "user" : "gomer", "article" : "aaa" }, "matches" : false }
{ "_id" : { "a" : null }, "user" : { "_id" : 3 }, "article" : { "_id" : 1, "user" : "gomer", "article" : "aaa" }, "matches" : false }
{ "_id" : { "a" : null }, "user" : { "_id" : "gomer", "group" : [ "user", "author" ] }, "article" : { "_id" : 2, "user" : "vasya", "article" : "bbb" }, "matches" : false }
{ "_id" : { "a" : null }, "user" : { "_id" : "vasya", "group" : [ "user" ] }, "article" : { "_id" : 2, "user" : "vasya", "article" : "bbb" }, "matches" : true }
{ "_id" : { "a" : null }, "user" : { "_id" : 1 }, "article" : { "_id" : 2, "user" : "vasya", "article" : "bbb" }, "matches" : false }
{ "_id" : { "a" : null }, "user" : { "_id" : 2 }, "article" : { "_id" : 2, "user" : "vasya", "article" : "bbb" }, "matches" : false }
{ "_id" : { "a" : null }, "user" : { "_id" : 3 }, "article" : { "_id" : 2, "user" : "vasya", "article" : "bbb" }, "matches" : false }
Type "it" for more


І завершальна частина:
{ $match: { matches:true } }

Остаточний результат{ "_id": { «a»: null }, «user»: { "_id": «gomer», «group»: [ «user», «author» ] }, «article»: { "_id": 1, «user»: «gomer», «article»: «aaa» }, «matches»: true }
{ "_id": { «a»: null }, «user»: { "_id": «vasya», «group»: [ «user» ] }, «article»: { "_id": 2, «user»: «vasya», «article»: «bbb» }, «matches»: true }
{ "_id": { «a»: null }, «user»: { "_id": «gomer», «group»: [ «user», «author» ] }, «article»: { "_id": 3, «user»: «gomer», «article»: «ccc» }, «matches»: true }

Ми просто відфільтруємо ті документи, які відповідали попереднім критерієм.
І тепер у нас є документи, в яких зазначено яку статтю написав користувач і в якій настроюваної групи він складається.

Звичайно, запит можна було б написати трохи компактніше але завданням було показати як можна гратися з даними допомогою
pipeline
.

7. Приклади
В основному тут мова піде швидше про структуру документів ніж про запити. Як правило є два основних підходу зазвичай:
  • Зберігання передбачуваних поддокументов або просто полів, за якими буде здійснюватися пошук всередині кореневого або основного документа.
  • Зберігання окремо, по можливості намагаючись якомога більше заносити в них додаткової інформації.
Дерева, коментарі
Як правило, одна з найпоширеніших завдань — це різні деревоподібні структури. Це і коментарі, і каталог товарів в інтернет магазинах, і схеми зберігання на складах, і багато чого ще.



Розглянемо декілька простих прикладів.
У вигляді субдокументов:
{
_id:1, type:"blog", title:{uk:"O MongoDB", en:""}, 
comments: [
{ _id: 1, title: "one", "user": "Alex", parent: "root", child: [2, 3]},
{ _id: 2, title: "two", "user": "Serg", parent: 1 }, 
{ _id: 3, title: "two", "user": "Andrey", parent: 1 }
]
}

Просто документи:
{ _id: 1, type: "comment", title: "one", "user": "Alex", parent: "root", child: [ 2, 3 ] },
{ _id: 2, тип: "comment", title: "two", "user": "Serg", parent: 1 }, 
{ _id: 3, тип: "comment", title: "two", "user": "Andrey", parent: 1 }

Сам по собі простий пошук по обом варіантам буде працювати досить ефективно.
Тим більше, що за другим варіантом можна створити індекси.
У деяких випадках варіант з поддокументами потрібен для зберігання інформації, але в цілому самі коментарі зберігаються в поддокументах, такі варіанти називають модним нині словом «гібридна» схема.

Тепер розглянемо кілька прикладів роботи з документами.
Видалити одну або декілька дітей у батька:

db.test.update( { _id: 1 }, { $pull: { child: 2 } } )

db.test.update( { _id: 1 }, { $pullAll: { child: [ 2, 3 ] } } )

Додати одну або декілька дітей батьків:

db.test.update( { _id: 1 }, { $push: { child: 2 } } } )

db.test.update( { _id: 1 }, { $push: { child: { $each: [ 2, 3 ] } } } )


Сформуємо дерево з отриманих документів:
def getTree(docs):
tree = { doc["_id"]: doc for doc in docs }
for doc in docs:
doc['child'] = []
for doc in docs:
parent = doc["parent"]
if parent != "root":
tree[parent]["child"].append(doc)
docs={"_id": "root", "child": [doc for doc in docs if doc['parent'] == "root" ]}
return docs


{ _id: 1, type: "comment", title: "one", "user": "Alex", parent: "root", 
child: [ 
{ _id: 2, тип: "comment", title: "two", "user": "Serg", parent: 1 },
{ _id: 3, тип: "comment", title: "two", "user": "Andrey", parent: 1 }
] 
}


Порахуємо кількість товарів, які відносяться до кожної категорії дерева з урахуванням всіх вкладених категорій, за умови, що в кожному товарі зберігається
_id
категорії до якої він належить:
def count(cls):
db = connect()
ctr = db.test.find({'type':'goods', 'class':cls}).count()
childs = db.test.find_one({'_id':cls})
for res in childs['child']:
ctr += count(res)
return ctr

Знайдемо шлях до початку, знаючи
_id
категорії, після чого залишиться в шаблоні просто фором пройтися по списку кортежів:
def path( id ):
p = []
parent = db.test.find_one( {"_id": id }, { "parent": 1, "alias":1, "title":1})
else:
path.append( ( parent['alias'], parent['title'] ) )
p += path( parent['parent'] )
return p
print ( path("123") )

>>>[ ("one", "Перша гілка"), ("two", "Друга гілка") ]


Теги, блоги
Найчастіше для зберігання тегів використовується гібридний варіант, тобто вони зберігаються наприклад у полі
{ tags : { uk: "один, два" } }
, щоб їх відразу було зручно виводити на сторінці під матеріалом. І зберігаються в масиві для зручного пошуку з ним
{ tags : [ "", "два" ] }
.
Хмари тегів іноді теж окремо зберігають, іноді формують на льоту.
Пошук по масиву тегів, якщо, наприклад, потрібно знайти список документів:

{ _id: 1, title: "Мови програмування", tags: [ "php", "python" ] }

db.test.find({ tags: { $in: ["php", "python" ] } } )


Ну і тепер відфільтруємо:
  • По тегам, наприклад, хочемо, щоб у цьому місці показувалися документи з тегами
    python, javascript
    .
  • І, в теж час, не показувалися документи де згадуються теги, наприклад, «реклама».
  • За типом, врахуємо який тип контенту треба показати, наприклад, будемо показувати новини.
  • Врахуємо користувачів за чиїм авторством можна показувати документи.
  • Також встановимо, що більше одного документа для одного користувача не можна показувати.
  • Встановимо термін давності, щоб виводилися документи, яким не більше, наприклад, 5 днів давності.
  • Мінімальний рейтинг показу, тобто документи у яких рейтинг менше
    +2
    не показувати.
  • Перевіримо щоб документи були оприлюднені та схвалені адміністратором.
  • Встановимо ліміт
    10
    штук для показу.
  • Упорядкуємо за переглядами, і ті, які промарковані спеціально (іноді буває потрібно).


dt = ( datetime.today() + timedelta( days = -5 ) )
db.test.aggregate([
{ $match: {
// обираємо тип контенту і термін давності
type: "news", date: { $gt: dt }, 
// обираємо мінімальний рейтинг.
vate: { $gte: 2 }, 
//Матеріали яких користувачів показувати.
user: { $in: [ "alex", "pavel" ] } 
$and: [ 
// Документ дозволено до публікації і схвалений.
{ pub: true }, { accept: true }, 
// Обираємо за яким тегам потрібно відфільтрувати документи.
{ tags: { $in: ["php", "python" ] } } , 
// Теги з якими ми не хочемо бачити документи.
{ tags: { $nin: ["реклама"] } }
]
},
// Сортуємо по важливості і по даті.
{ $sort: {'primary': -1, view: -1}},
// Встановлюємо ліміт документів, показувати не більше 5 
{ $limit:3},
// Показувати не більше 1 документа для кожного користувача, групуємо за користувачеві.
{ $group: { 
'_id':'$user',
'id': {'$first':'$_id'},
'type':{'$first':'$type'},
'title': {'$first':'$title'},
'content':{'$first':'$content'},
'count':{'$first':'$count_comm'},
'last_comm':{'$first':'$last_comm'},
'vote':{'$first':'$vote'},
'tags':{'$first':'$tags'}
}
},
// згруповані документи наводимо в потрібний нам вигляд.
{ $project :{
'_id':'$id', 'title':1, 'content':1, 'type':1, 'count':1, 'last_comm':1, 'tags':1, 'vote':1
}
}
])

Запит без коментарів
db.test.aggregate([
{ $match: {
type: "news", date: { $gt: dt }, vate: { $gte: 2 }, user: { $in: [ "alex", "pavel" ] } 
$and: [ 
{ pub: true }, { accept: true }, 
{ tags: { $in: ["php", "python" ] } } , 
{ tags: { $nin: ["реклама"] } }
]
},
{ $sort: {'primary': -1, view: -1}}, { $limit:3},
{ $group: {'_id':'$user',
'id': {'$first':'$_id'}, 'type':{'$first':'$type'},
'title': {'$first':'$title'}, 'content':{'$first':'$content'},
'count':{'$first':'$count_comm'},
'last_comm':{'$first':'$last_comm'},
'vote':{'$first':'$vote'}, 'tags':{'$first':'$tags'}
}
},
{ $project :{
'_id':'$id', 'title':1, 'content':1, 'type':1, 'count':1, 'last_comm':1, 'tags':1, 'vote':1
}
}
])


Тепер все, що нам потрібно — розділити область видимості сайту на елементи і для кожної з них можна надавати свій контент.

Е-соммегсеи, фільтри
Напевно, одна з основних труднощів і завдань веб-магазинів і різних обліків — це все-таки фільтри. Вони потрібні як для створення хитрих звітів, так і просто для показу де що лежить на складі, ну і, банально, відфільтрувати ноутбуки по діагоналі, ціною, виробнику і іншого.

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

Існує два види фільтрів, або, якщо хочете, способу організації контенту у веб-магазині.
  1. Неправильний і не вимагає нічого (часто досить просто спарс прайс товари або взагалі з якогось сайту )
  2. Правильний і потребує детальної настройки користувачем.


У першому випадку, оскільки наявні атрибути у нас просто текстові поля, ні з чим не пов'язані, то нам потрібно просто за ним згрупувати документи — і все. Але в такому випадку при неправильному заповненні, а також при розвитку обліку, в магазині виникнуть проблеми і доведеться все перезаносить.

У другому випадку нам треба для кожної категорії товарів налаштувати свої фільтри і кожен з фільтрів тим чи іншим способом прив'язати до характеристик товарів (атрибутів).

Робота з фільтрами складається з двох етапів:
  1. Показ значень фільтра в залежності від категорії, тобто категорії монітор є діагональ а у категорії процесор, тактова частота.
  2. І пошук цих значень.


Розпочнемо з показу значень фільтру, якщо у нас налаштована прив'язка фільтрів до характеристик товарів, тоді все виглядає досить просто. І робиться простим запитом:
cursor = db.test.find({ "type": "filters", "category": "id_category" })

Представити дані можна по різному, наприклад, можна зробити деревоподібної таблицею, де першим рівнем будуть назви фільтрів, а другим рівнем його характеристики.
Назва => Діагональ
Характеристики=> 15.6 дюймів, 17 дюймів і тд.



Можна по іншому уявити, як піти на мій погляд трохи більш поширеним шляхом. Зробити подтаблицами за типом прибуткових і витратних накладних. Коли є шапка документа з переліком реквізитів і тд., є список товарів знизу.
Тільки в даному випадку шапка — це назва категорії, для якої ми налаштовуємо фільтри, а список товарів — це перелік назв фільтрів з атрибутами в однієї з підлеглих таблиць.



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

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



У нашому випадку візьмемо трохи більш складний варіант і припустимо, що це будуть окремі документи. Бажано щоб _id категорії зберігалося тоді в кожному документі з характеристикою.
Оператор
$addToSet
при групуванні обирає унікальні елементи з масиву.
db.test.aggregate([
// знаходимо всі документи за типом довідника по id категорії 
{ '$match': { type : "goods_attr", category: id_category } },
// трохи переформатуємо документи для більш зручної угруповання, відкидаючи зайві поля 
{ '$project': { "title" : "$title.ua", 'value': "$attr.ua", 'category': "$category", '_id': 0 } },
{ '$group' : {
'_id': { 'category' :"$category", 'title': "$title"} , 
'filters': { '$addToSet': "$value" } 
} 
},
{ '$group' : {
'_id' :"$_id.category", 'title':{ '$addToSet': { 'title': "$_id.title", 'filters': "$filters" } } 
} 
}
])

Після першого group ми отримуємо документи такого типу:
...{ '$group' : {
'_id': { 'category' :"$category", 'title': "$title"} , 
'filters': { '$addToSet': "$value" } 
} 
}....

{ "_id": { "category": "id", "title": "Стійкість до агресивних середовищ" }, "filters": [ "Так" ] }
{ "_id" : { "category" : "id", "title" : "Матеріал" }, "filters" : [ "Нержавіюча сталь" ] }
{ "_id" : { "category" : "id", "title" : "Тип затискача" }, "filters" : [ "Натяжна" ] }


Після другого group ми вже отримуємо остаточний варіант.
...{ '$group' : {
'_id':"$_id.category", 'title':{'$addToSet': {'title': "$_id.title", 'filters': "$filters" }} 
} 
...}

{ 
"_id" : "id_category", 
"title" : [ 
{ "title" : "Тип арматури для ЛЕП", "filters" : 
[ "Гак для ЛЕП", "Скріпа для ЛЕП", "Стрічка бандажна", "Зажим для ЛЕП" ] 
}, 
{ "title" : "Тип затискача", "filters" : [ "Натяжна" ] }, 
{ "title" : "Матеріал", "filters" : [ "Нержавіюча сталь" ] }, 
{ "title" : "Стійкість до агресивних середовищ", "filters" : [ "Так" ] } 
] 
}



Тепер, коли ми вже маємо список фільтрів, можна пошукати за цим фільтрам. Оскільки ми шукаємо за субдокументам, тобто за характеристиками, то групуємо
owner_id
це _id документа до якого належать характеристики.

db.test.aggregate([
{ '$match' : 
{ 'type' : "goods_attr", "category":"id", 
'$or': [
{'title': 'Матеріал', 'attr': 'Нержавіюча сталь'}, 
{'title': 'Тип арматури для ЛЕП', 'attr_val': 'Гак для ЛЕП'}
] 
} 
},
{ '$group': {'_id': "$owner_id", "attr": { '$push': "$title" }}},
{ '$match': {"attr": {'$all': [ 'Матеріал', 'Тип арматури для ЛЕП' ] }}},
{ '$project': {"_id":1 } }
])


На виході отримуємо список _id документів підходять під фільтри.

Невелика пісочниця для Python
Як правило при роботі з різними типами даних зручно візуалізувати у вигляді різних ієрархічних та інших табличок.

Але часто буває, що потрібно повісити на зміну, наприклад, поля або вилучення документа (читай — рядки в таблиці) якусь подію. Наприклад, ми поміняли в довіднику валют курс за яким закуповуємо, і хочемо щоб ціни у вітчизняній валюті перерахувалися для всіх товарів.

Це досить стандартний функціонал для різних ERP рішень. І, оскільки часто невідомо хто буде писати для пісочниці код, потрібна можливість запускати чужий код більш-менш безпечно. На даний момент є бібліотеки, які дають цю можливість, але точно невідомо як добре вони обмежують запускається код.

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

Приклад з
exec

src = "'
result = 0
for i in xrange(100):
result += i
"'

assert '__' not in src, 'Prohibited to use symbols "__"'
pr = compile(src, '<string>', mode='exec')
glob = { '__builtins__':{ 'xrange':xrange } }
exec(pr, glob)
print glob['result']

Приклад з
eval
(для обчислення виразу)
src = 'max(5,7,3)'
glob = { '__builtins__':{ 'max':max } }
assert '__' not in src, 'Prohibited to use symbols "__"'
print ( eval(src, glob) )


Головне — це:
assert '__' not in src, 'Prohibited to use symbols "__"'

— заборона доступу до спец об'єктів, на подобі:
__class__, __base__

через які можна отримати повний доступ до python та
glob = { '__builtins__':{ 'xrange':xrange } }

тут ми замінюємо весь базовий функціонал (import, type...) на потрібний/дозволений функціонал.

Так само не можна давати використовувати метод
getattr
.

Зараз це вважається більш менш надійним варіантом
sandbox
.

P. S. По можливості намагався скорочувати, насправді по кожному пункту можна було написати окрему публікацію. Прохання про помилки граматичного та іншого характеру писати в лічку, так як стаття вийшла довга, міг щось не помітити.

Використовувані матеріали.

Довідка за mongodb
Оператори
Все про текстовий пошук
Довідка за PyMongo
Оновлення mongodb до версії 2.6
Оновлення mongodb до версії 3.0
Порівняльні приклади SQL запитів і запитів через aggregation pipeline
Стаття на хабре про aggregation pipeline
Трохи про транзакції


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.