Туториал по Schevo ∞
Давно собирался рассказать о Schevo, да всё никак руки не доходили. Исправляюсь...
Schevo (шево) - это реляционная надстройка над ООДБ (бэкенды - Durus и ZODB), разрабатываемая Orbtech. Самоопределение Schevo таково (вольный перевод):
Schevo - это СУБД следующего поколения, основными достоинствами которой являются:
- Быстрая разработка. Легко и просто создавайте даже достаточно сложные базы. Схема легко пишется, легко читается. Можно быстро указывать начальные данные прямо в схему; используя тот же синтаксис можно создать набор тестовых данных для разработчика.
- Богатое описание схемы. Описывайте схемы данных, используя лаконичный, легко читаемый Python-код. Схема описывает не только структуру БД, но и все нестандартные транзакции и правила непротиворечивости данных.
- Автоматическая эволюция схемы. Спокойно используйте Schevo для хранения часто изменяемых данных. При необходимости, изменяете схему и при помощи инструментов Schevo легко мигрируйте от одной версии схемы к другой.
- Транзакции. Schevo защищает ваши данные. Единственный способ изменить данные - это явные транзакции. Вы можете доверить Schevo свои данные, оин всегда будут в консистентном состоянии.
- Генерация UI. Код пользовательского интерфейса использует преимущества схемы. Используйте полнофункциональный навигатор по БД без необходимости писать какой-либо код (вне вашей схемы) вообще. Создавайте кастомизированный интерфейс при помощи специальных Schevo-виджетов и инструментов.
Я расскажу о Schevo в формате "методички" и постараюсь пройтись по всем бенефитам, перечисленным выше. Я наверняка пропущу какие-нибудь моменты, но цель сориентировать по технологии, а не написать справочник. Естественно, прежде чем приступать к изучению Schevo, следует его установить. Это легко сделать при помощи easy_install Schevo
Схема
Схема приложения должна находится в модуле <yourapp>.schema.<schema_name>__<schema_version>. Например, схема версии 1, для pyobject лежит в pyobject.schema.pyobject_001. Любая схема начинается с "магических строк"
from schevo.schema import *
schevo.schema.prep(locals())
В Schevo есть специальная поддержка иконок для ваших объектов, я их не использовал, так что промолчу.
Запрещенные имена в схеме:
- Однобуквенные имена. Зарезервированы для пространств имен (namespaces)
- Начинающиеся с подчеркивания. Зарезервированы для приватных методов Schevo
-
Имена вида буква_имя, например
t_something. Зарезервировано за запросами (query), транзакциями (transaction), видами (view) и методами-расширениями (extender). -
classmethod,extentmethod,db,schevo,sys. Зарезервированные имена.
Схема
В Schevo используются мнемонические однобуквенные пространства имен (namespaces) для различных целей. В описании схемы у меня будут использоваться:
-
E: Entity. Пространство сущностей. -
F: Field. Пространство типов полей. -
f: Field definition. Пространство конструкторов полей.
К примеру, сущность "Статья в блоге" я описал так:
class Post(E.Entity):
"""Blog post entity"""
title = f.unicode()
slug = f.string()
abstract = f.memo()
body = f.memo(allow_empty=True, required=False)
created_at = f.datetime()
_index(created_at)
_key(slug)
С типами полей, думаю, всё понятно. _index, это индекс (дает возможность по этому полю делать быструю сортировку), _key - это ключевой атрибут сущности. В данном случае это slug.
Связь один-ко-многим реализуется типом поля entity. Поясню примером сущности "Комментарий к статье в блоге":
class Comment(E.Entity):
"""Comment on blog post entity"""
post = f.entity('Post', CASCADE)
author = f.unicode()
author_email = f.unicode()
author_site = f.unicode(required=False, allow_empty=True)
body = f.memo()
created_at = f.datetime()
_index(created_at)
_key(author, author_email, created_at, body)
Поле entity позволяет связывать несколько типов сущностей.
Отношение много-ко-многим, как и в традиционных РСУБД, реализуется промежуточной сущностью. Примером отношений много-ко-многим могут служить теги.
class Tag(E.Entity):
"""Tag for blog post entity"""
name = f.unicode()
slug = f.string()
_key(name, slug)
class PostTag(E.Entity):
"""Many-to-many relation between Post and Tag"""
tag = f.entity('Tag')
post = f.entity('Post')
_key(tag, post)
Давайте попробуем чуть "поиграться" тем, что у нас есть. Для этого создадим приложение BlogTut:
$ paster create BlogTut -t schevo
и в схему добавим вышеописанные сущности.
Работа с БД
Далее, необходимо создать БД для нашего приложения (имя приложения пишется в нижнем регистре)
$ schevo db create --app=blogtut dev.db
Schevo 3.1a1dev-r3564 :: Database Activities :: Create Database
Creating new database at version 1.
Packing the database...
Database version is now at 1.
Database created.
Теперь можно запустить интерактивную сессию для работы с БД:
Если у вас стоит ipython, то именно он будет использоваться для этой сессии. Но у ipython есть проблемы с юникодом, поэтому будьте вдвойне внимательны. А я же, чтобы избежать проблем, буду использовать значения в латинице :)
Наша БД будет доступна в объекте db.
$ schevo shell dev.db
Schevo 3.1a1dev-r3564 :: Python Shell
Opened database dev.db
Python 2.4.4 (#2, Apr 5 2007, 20:11:18)
Type "copyright", "credits" or "license" for more information.
IPython 0.7.2 -- An enhanced Interactive Python.
? -> Introduction to IPython's features.
%magic -> Information about IPython's 'magic' % functions.
help -> Python's own help system.
object? -> Details about 'object'. ?object also works, ?? prints more.
>>> db
<<< <Database u'Schevo Database' :: V 1>
Давайте создадим пост. Как уже выше сказано, все изменения в Schevo делаются через транзакции (которые находятся в пространстве имен t).
>>> tx = db.Post.t.create()
>>> tx.title = u"Hello world"
>>> tx.slug = "hello-world"
>>> tx.abstract = u"Just say hello to everyone!"
>>> tx.created_at = datetime.datetime.now()
>>> post = db.execute(tx)
>>> post
<<< <Post entity oid:1 rev:0>
Давайте аналогичным образом добавим и комментарий:
>>> tx = db.Comment.t.create()
>>> tx.author = u"Pythy"
>>> tx.author_email = u"the.pythy@gmail.com"
>>> tx.author_site = u"http://www.pyobject.ru"
>>> tx.created_at = datetime.datetime.now()
>>> tx.body = "Just comment"
>>> db.execute(tx)
AttributeError: post value is required by Create :: Comment
# упс, забыли про то, на какой же пост ссылается комментарий
>>> tx.post = post
>>> db.execute(tx)
<<< <Comment entity oid:1 rev:0>
Начальные (тестовые) данные
Понятно, что каждый раз для тестовых (или начальных) данных писать каждый раз руками вставку значений - утомительно, поэтому радуемся что их можно указать внутри схемы:
E.Post._sample = [
(u"Hello, World", "hello-world", u"Just say hello to everyone!", DEFAULT, "2007-09-27 11:14"),
(u"Test", "test", u"Test, please ignore it", "The body of test post", "2007-09-28 17:36"),
]
E.Comment._sample = [
(("hello-world", ), u"Pythy", u"the.pythy@gmail.com", u"http://www.pyobject.ru", u"Firstf**k", "2007-09-27 12:42"),
(("hello-world", ), u"DummyCommenter", u"the.pythy@gmail.com", DEFAULT, u"+1", "2007-09-28 00:15"),
(("test", ), u"DummyCommenter", u"the.pythy@gmail.com", DEFAULT, u"+1", "2007-09-28 00:22"),
]
E.Tag._sample = [
(u"Hello", "hello"),
(u"Test", "test"),
]
E.PostTag._sample = [
((u"Hello", "hello"), ("hello-world", )),
((u"Test", "test"), ("hello-world", )),
((u"Test", "test"), ("test", )),
]
Мне кажется, что всё предельно прозрачно. Объекты, на которых необходимо сослаться (например, пост), определяются по ключевым атрибутам (для поста - слуг, для тега - пара имя-слуг).
Получить заполненную базу можно так:
$ schevo db create --delete --sample --app=tutblog dev.db
Schevo 3.1a1dev-r3564 :: Database Activities :: Create Database
Creating new database at version 1.
Populating with sample data...
Database version is now at 1.
Database created.
Для начальных данных следует использовать атрибут _initial.
Эволюция
Как то видеть объекты в виде <Post entity oid:1 rev:0> не очень здорово. Скопируем схему в blogtut_002.py, добавим методы __unicode__ и __repr__ чтобы объекты были красивей. Я здесь код приводить не буду, это банально. Гораздо интересней как это "запихать" в базу, поскольку схема читается только при создании базы (ну и еще при других подобных операциях, но я умолчу об этом). После того как база создана, она является вещью в себе, БД содержит внутри в том числе и описание схемы (db.schema, db.schema_source).
Если б мы не изменили версию, а поменяли бы схему без изменения версии, то тогда нужно делать обновление (update) базы, мы же поменяли версию схемы, то нужно делать эволюцию (evolve):
$ schevo db evolve --app=tutblog dev.db 2
Schevo 3.1a1dev-r3564 :: Database Activities :: Evolve Database
Current database version is 1.
Read schema source for version 2.
Evolving database to version 2...
Database evolution to version 2 complete.
Packing the database...
Database evolution complete.
Посмотрим, что получилось:
>>> db.Post.find()
<<<
[<Post: hello-world (2007-09-27 11:14:00) oid:1 rev:0>,
<Post: test (2007-09-28 17:36:00) oid:2 rev:0>]
>>> list(db.Post.by('-created_at'))
<<<
[<Post: test (2007-09-28 17:36:00) oid:2 rev:0>,
<Post: hello-world (2007-09-27 11:14:00) oid:1 rev:0>]
>>> db.Post.find(slug='test')
<<< [<Post: test (2007-09-28 17:36:00) oid:2 rev:0>]
>>> post = db.Post.findone(slug='hello-world')
Как же посмотреть, какие комментарии относятся к данному посту? При помощи пространства имен m (one-to-many namespace)
>>> post.m.comments()
<<<
[<Comment: u'Pythy' on hello-world at 2007-09-27 12:42:00 oid:1 rev:0>,
<Comment: u'DummyCommenter' on hello-world at 2007-09-28 00:15:00 oid:2 rev:0>]
Заметьте, что мы описывали сущность
Comment, а метод называетсяcomments. В Schevo есть правила преобразования единственного числа в множественное, но если Schevo спотыкается, можно явно указать, определив атрибут_plural.
Или теги:
>>> [pt.tag for pt in post.m.post_tags()]
<<< [<Tag: u'Hello' (hello) oid:1 rev:0>, <Tag: u'Test' (test) oid:2 rev:0>]
Так не очень удобно. Здесь бы удобно было сделать дополнительный метод, который и возвращал бы список тегов. У Schevo для пользовательских методов есть специальное пространство имен - x (extenders). Так что к сущности "Блог пост" добавим свой метод:
def x_tags(self):
return (posttag.tag for posttag in self.m.post_tags())
И он будет доступен как post.x.tags. Не будем менять версию схемы, просто обновим:
$ schevo db update --app=blogtut dev.db
Schevo 3.1a1dev-r3564 :: Database Activities :: Update Database
Opening database...
Current database version is 2.
Syncing database with new schema source...
Packing the database...
Database updated.
Транзакции
В Schevo любое изменение можно сделать только при помощи явных транзакций, которые находятся в пространстве имен t (transaction). Для создания -- транзакции сущности, для обновления/удаления - транзакции объекта. После подтверждения транзакции возвращается сохраненный/измененный объект. С созданием объекта уже пример был, изменение/удаление:
>>> post = db.Post.findone(slug='test')
>>> tx = post.t.update()
>>> tx.body = u"Just test, nothing else"
>>> db.execute(tx)
<<< <Post: test (2007-09-28 17:36:00) oid:2 rev:1>
Все бэкенды Schevo: Durus и ZODB (ZODB был добавлен относительно недавно, Durus был изначально) - при изменении объектов не удаляют старые, а добавляют новые ревизии объектов. Для удаления устаревших ревизий служит процедура упаковки (db.pack), которая автоматически выполняется при обновлении/эволюции.
>>> comment = db.Comment.findone(author_site=u"http://www.pyobject.ru")
>>> tx = comment.t.delete()
>>> db.execute(tx)
>>> comment
EntityDoesNotExist: "OID 1 does not exist in 'Comment'"
Транзакции можно настраивать. Например, хочется, чтобы при создании комментария отправлялось письмо администратору. В Django такое делается при помощи сигналов PyDispatch и обработчиков. Schevo зависит от форка PyDispatch - Louie, так что тоже воспользуемся сигналами. Внутри сущности "Комментарий к постам в блоге" создадим класс:
class _Create(T.Create):
def _after_execute(self, db, entity):
louie.send(signal='on_create_comment', sender=self.__class__, db=db, entity=entity)
Подробнее о кастомизированных транзакциях смотрите в блоге ROTR.
Код примера, как всегда, на code.google.com.
За бортом.
За бортом остались: расширенные запросы из пространства имен Q (query):
>>> matched = db.Q.Match(db.Post, 'slug', 'startswith', 'he')
>>> print matched
Posts where Slug starts with he
>>> list(matched())
<<< [<Post: hello-world (2007-09-27 11:14:00) oid:1 rev:0>]
GTK2 CRUD-интерфейс к Schevo:
и пример использования Schevo - Twitabit.
Проблемы
В целом, Schevo оставляет приятное впечатление (идея, реализация). Очень сильно хромает документация, на обе ноги. Всё что есть в комплекте - читать обязательно.
Проблема номер два: оба бэкенда (ZODB, Durus) блокируют файлы при открытии БД. Это означает, что они thread-safe, но не process-safe. Чтобы "обойти" это, ООДБ предлагают запускать сервер (DurusServer, ZEO), который монопольно открывает БД, и к серверу уже коннектятся клиенты, используя ClientStorage. Проблема в том, что Schevo "заточен" под локальные хранилища. Для pyobject.ru это вышло боком. Пришлось модифицировать функцию открытия БД в Schevo. Но ввиду агрессивного кеширования внутри каждого соединения, при модификации объектов вылезают различные косяки.
Субъективно
Субъективно, ниша Schevo - десктопные приложения.
