Вечер добрый. На прошлой неделе стартовал курс «MongoDb for developers» от 10gen, о котором уже писали на хабре. Если вы смотрели уроки, то можете смело проходить мимо. Остальным — добро пожаловать.
В этой статье будет изложен основной материал первой недели обучения. Если аудитория проявит интерес — то подобные посты будут выходить в конце каждой недели.
Мы вкратце рассмотрим, что представляет собой MongoDB, сравним разницу в структурах данных между монго и реляционными базами для простого веб-приложения, поиграемся с шеллом, и немножко покодим на пхп и питоне.
Зачем эта статья? Предвижу подобный вопрос. Не все успели записаться на курсы, не у всех есть достаточно свободного времени, не у всех хорошо обстоят дела с восприятием устной английской речи. Ну и для гуглящих подобный материал не помешает.
МонгоДб — не реляционная база данных, предназначенная для хранения JSON-документов. Рассмотрим некий абстрактный экземпляр коллекции монго
{a:3
b:7
fruit:["apple","banana","peach"]}
Мы видим, что данный документ имеет целочисленные ключи a и b и ключ fruit который является массивом. В реляционных базах подобное поведение невозможно. Стоит отметить, что подобное представление данных гораздо ближе к коду, чем то с которым мы имеем дело работая с реляционными базами данных.
Коллекции в МонгоДб не привязаны к заранее определенной схеме (schemaless). Например следующие документы, могут быть элементами одной коллекции
{a:1,b2}
{a:2,b:4,c:10}
Вспомните, сколько раз вам приходилось скрипя зубами делать ALTER TABLE, добавляя новое поле, которое то и использовать будут далеко не все элементы? С МонгоДБ подобное осталось в прошлом.
Если посмотреть на криво-нарисованную схемку ниже, видно что в мире хранения данных, есть два противоположных полюса. Шустрые, но бедные функциональностью хранилища ключ-значения (такие как memcached) и крайне функциональные реляционные базы данных, имеющие впрочем проблемы с производительностью и масштабируемостью
Одна из основополагающих идей MongoDb — предоставлять широкий набор возможностей, при сохранении высокой производительности. Конечно чем-то приходится жертвовать. Так например Монго не имеет аналога join: нельзя объединить элементы двух разных коллекций. Вторым важным отличием от реляционных баз данных является отсутствие транзакций. Последнее звучит пугающе, но дело в том, что в монго вам не понадобятся транзакции, в тех ситуациях где они были бы необходимы при использовании реляционной базы.
От слов к делу
Для продолжения нам потребуется установить MongoDb. Вы скорее всего найдете свежую версию в репозитории своего дистрибутива, в случае отсутствия таковой- исходники доступны на официальном сайте. Там же есть бинарники для win и mac. Стоит отметить, что 32х-битная версия имеет существенные ограничения по объему хранимых данных, так что ее можно использовать только для разработки. Сейчас для нас это не принципиально.
Устанавливаем, запускаем и заходим в шелл.
dirtyhack@dirtyhack-ThinkPad-Edge-E125:~$ mongo
MongoDB shell version: 2.0.4
connecting to: test
>
Пока у нас нет никаких данных, монго создал для нас базу test, с которой мы и будем работать. Положим в нее наш первый элемент
> db.users.save({name:"Woody",age:23});
> db.users.find().pretty()
{
"_id" : ObjectId("508ea7f33cc5578ed9ecbf46"),
"name" : "Woody",
"age" : 23
}
>
Тут сразу много интересного. Командой db.users.save() мы отправили запрос на сохранение документа в коллекцию users текущей базы данных. Ранее этой коллекции не существовало, она была автоматически создана при запросе.
> show collections
system.indexes
users
>
Мы сохранили в коллекции users простой json-документ, с ключами name и age. О назначении команды find, догадаться несложно — стоит отметить, что в случае когда нас интересует только один документ стоит пользоваться командой findOne(). Команда pretty() не влияет на логику- она просто выводит результат в удобном для чтения виде — без нее, мы бы получили строку, что не удобно при работе со сложными объектами.
Пришло время добавить в коллекцию элемент посложнее, и наглядно продемонстрировать безсхемность монго.
> db.users.save({name:"Bazz",age:23,place_of_birth: {country:"unknown",region:"unknown"},interests:["flying","fighting with evil"]});
> db.users.find({name:"Bazz"}).pretty()
{
"_id" : ObjectId("508eab333cc5578ed9ecbf47"),
"name" : "Bazz",
"age" : 23,
"place_of_birth" : {
"country" : "unknown",
"region" : "unknown"
},
"interests" : [
"flying",
"fighting with evil"
]
}
>
Второй добавленный нами документ уже более похож на реальные данные. Обратите внимание, что значениями элементов place_of_birth и interests являются вложенные документы- словарь (JSONObject) и массив (JSONArray) соответственно. Иерархическая вложенность — ограничивается здравым смыслом и лимитом в 16 мегабайт на элемент.
Кстати, мы можем исgользовать встроенный интерпретатор javascript
> var j=db.users.findOne({name:"Woody"})
> j.age=25
25
> db.users.save(j)
> db.users.find({name:"Woody"}).pretty()
{
"_id" : ObjectId("508ea7f33cc5578ed9ecbf46"),
"name" : "Woody",
"age" : 25
}
>
Вы наверное уже заметили, что каждому элементу присваивается идентификатор _id. О нем мы поговорим несколько позже, отметим только, что он уникален в пределах одной коллекции.
Полученную коллекцию мы используем в небольшом приложении, в завершении статьи.
Проектирование
Рассмотрим как будет выглядеть структура данных простого веб-приложения в Монго и реляционной базе данных.
Представим, что у нас есть блог с базовым функционалом. Авторы могут публиковать посты, прикрепляя к ним теги, пользователи могут искать по тегам и оставлять к постам комментарии.
При использовании реляционной базы данных нам понадобятся таблицы для хранения данных об авторах, постах и комментариях. Так же нам, скорее всего, понадобятся дополнительные таблицы для хранения связей между таблицами. Конечно сообразно своим реалиям, вы можете денормализовать базу блога, но в базовом виде, она будет выглядеть как-то так.
В монгоДБ для хранения аналогичных данных, нам будет достаточно двух коллекций. Надеюсь почерк мистера Эрликсона, на картинке достаточно разборчив.
Если вы обратили внимание — в коллекции авторов в качестве уникального _id предлагается использовать логин. Да, идентификатор можно задавать (соблюдая уникальность конечно), в случае если он не задан — система сделает это за вас.
Как вы видите структура данных в монго заметно проще, и как я уже отмечал выше — значительно ближе к представлению данных в коде.
Вы наверное уже устали от моих излияний, поэтому еще одно замечание об идентификаторах, и покодим наконец.
Задание идентификатора — является операцией с высоким приоритетом. Он задается до фактического сохранения данных. По умолчанию драйвер монго не дожидается ответа от базы о сохранении данных, получает идентификатор и возвращает ответ пользователю. Если вас не устраивает такое поведение — вы можете использовать безопасный режим, но будьте готовы к некоторой потери производительности.
Пиши код, твою мать.
В уроках первой недели большую часть кода представили базовые занятия по питону и фреймворку bottle. В следующих занятиях мы поработаем с курсовым проектом блога на питоне и пхп, а пока создадим простенькое приложение, демонстрирующее работу с МонгоДб из кода.
Ввиду элементарности действий, мы не будем создавать никаких абстракций и сделаем все в лоб (коллеги наверное подумают, что я заболел). Думаю писать дао-прослойки вы прекрасно умеете сами.
Мы создадим простое веб-приложение выводящее на экран имена и возраст пользователей из коллекции users и позволяющее добавлять в коллекцию новые элементы.
Для запуска примера на питоне вам потребуется драйвер pymongo и фреймворк bottle. Для запуска примера на php, вам так же потребуется драйвер, его можно взять из pear(незабудьте добавить mongo.so в список расширений).
Итак примеры
Python
index.py
import bottle
import pymongo
import sys
# Открываем безопасное соединение с локальной базой
# При работе в безопасном режиме, в случае ошибки будет выброшено исключение, но их обработку мы рассмотрим позже
# там все просто
connection=pymongo.Connection("mongodb://localhost",safe=True)
# будем работать с нашей тестовой базой
db=connection.test
@bottle.route('/')
def home_page():
#Получаем все элементы коллекции
users=db.users.find();
return bottle.template('index',{"users":users})
@bottle.post('/submit')
def submit_page():
# подготавливаем элемент из данных формы
user={'name':bottle.request.forms.get("name"),'age':bottle.request.forms.get("age")}
#Сохраняем
db.users.insert(user)
bottle.redirect('/')
bottle.debug()
bottle.run(host="localhost",port=8080)
views/index.tpl
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
</head>
<body>
<p>
Список пользователей
</p><ul>
%for user in users:
<li>Имя:{{user['name']}} Возраст:{{user['age']}}</li>
%end
</ul>
<form action="/submit" method="POST">
Добавить нового пользователя<br>
<input type="text" name="name" size=40 value=""><br>
<input type="text" name="age" size=40 value=""><br>
<input type="submit" value="submit">
</form>
</body>
</html>
PHP
Пример на пхп отличается редкой незатейливостью
<?
$mongo = new Mongo('localhost');
$database = $mongo -> test;
if ($_POST)
$database -> users -> insert($_POST);
$users = $database -> users -> find();
?>
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
</head>
<body>
<?var_dump($users)?>
<p>
Список пользователей
</p><ul>
<?foreach($users as $user){?>
<li>Имя:<?=$user['name']?> Возраст:<?=$user['age']?></li>
<?}?>
</ul>
<form action="/" method="POST">
Добавить нового пользователя<br>
<input type="text" name="name" size=40 value=""><br>
<input type="text" name="age" size=40 value=""><br>
<input type="submit" value="submit">
</form>
</body>
</html>
Важный вопрос к тем, кто заинтересован в примерах на php. В дальнейшем код будет посложнее, можно использовать Yii или ZF, либо самописный микрофреймворк — жду ваших пожеланий.
На этом все, надеюсь не зря старался. Если статья понравится хабражителям — на след неделе, опубликую отчет по второй неделе, на которой будут рассматриваьтся CRUD операции.
Автор: sl4mmer