Fork me on GitHub
28/9/2007

Туториал по 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 - десктопные приложения.

Комментарии

Все статьи