Oct 27, 2011
|
Часто при проектировании структуры БД появляется необходимость повторять одни и те же действия с таблицами. Добавлять одинаковые поля, ссылки, счетчики и т.д. Модели в Pylons (SQLAlchemy), как и в большинстве других фреймворках использующих паттерн MVC, являются классом и соответственно могут быть унаследованы от других классов. Это позволяет нам избежать рутинной работы с повторяющимися действиями.
Все расширения для наших моделей будем добавлять в models/common.py
.
Создадим базовую модель в которой будет поле id
, автоматическая генерация
названия таблицы (__tablename__
) и метод выбора элемента по id
(SELECT * FROM
table WHERE id=integer
):
class Base(object):
"""Базовая модель. Добавляет во всех наследников поле id и атрибут
__tablename__ который заполняется автоматически. Имя таблицы берется из
названия класса и переводится в нижний регистр. Таблица наследник имеет по
умолчанию название и поле id, сильно облегчая жизнь.
"""
@declared_attr
def __tablename__(cls):
if (has_inherited_table(cls) and
Tablename not in cls.__bases__):
return None
return cls.__name__.lower()
# Method "byId" for use in code like this:
# session.query(Table).byId(5)
#
# SQL statement like:
# SELECT * FROM Table WHERE id = 5;
@classmethod
def byId(cls, id) :
return Session.query(cls).filter_by(id = id).first()
id = Column(Integer, autoincrement=True, primary_key=True)
Метод byId
сильно сокращает запись в коде например:
Session.query(net).filter_by(id = id).first()
net.byId(id)
Теперь создадим нашу модель унаследовав все плюшки с базовой модели:
class Net(Base, DeclarativeBase):
"""Net or subnet."""
cidr = Column(postgresql.CIDR, index = True)
description = Column(UnicodeText())
def __init__(self, cidr=''):
self.cidr = cidr
def __repr__(self):
return "%s" % self.cidr
Наша модель связанна с БД при помощи наследования от DeclarativeBase
, имеет
название, поле id
и метод byId
благодаря наследованию от базовой модели
Base
из файла common.py
.
Для более наглядного примера создадим типовую модель для таблиц которые должны содержать служебную информацию. Модель будет добавлять в другие модели поля:
created_by
- кто создалupdated_by
- последний кто обновилcreated_at
- дата созданияupdated_at
- дата последнего обновленияПоля заполняются автоматически. Кто создал и обновил ссылаются на модель
auth.User
. Пользователь берется из текущей сессии, при помощи библиотеки
lib.auth
и метода get_user
. Откуда взялась модель User
можно узнать из этой
статьи Авторизация в Pylons за 5 мин при помощи repoze.what.
class CreatedMixin(object):
"""Абстрактная примесь которая добавляет в другие модели поля:
created_by - кто создал
updated_by - последний кто обновил
created_at - дата создания
updated_at - дата последнего обновления
Поля заполняются автоматически. Кто создал и обновил ссылаются на модель
auth.User. Пользователь берется из текущей сессии, при помощи библиотеки
lib.auth и метода get_user
"""
@declared_attr
def created_by(cls):
return Column(Integer, ForeignKey('user.user_id',
onupdate="cascade", ondelete="restrict"))
@declared_attr
def updated_by(cls):
return Column(Integer, ForeignKey('user.user_id',
onupdate="cascade", ondelete="restrict"))
created_at = Column(DateTime, nullable=False, default=dt.now())
updated_at = Column(DateTime, nullable=False, default=dt.now(),
onupdate=dt.now())
Внешние ссылки и другие атрибуты отличающиеся от обычных полей нужно добавлять
при помощи декоратора declare_attr
. Теперь меняем нашу модель Net
просто
добавив CreateMixin
:
class Net(Base, DeclarativeBase, CreatedMixin):
Вот полный листинг common.py
:
# coding=utf-8
"""Модуль с типовыми моделями
"""
from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relation, relationship
from sqlalchemy.types import Integer, String, DateTime
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import has_inherited_table
from gottlieb.model.auth import User
from gottlieb.lib import auth
from datetime import datetime as dt
class Base(object):
"""Базовая модель. Добавляет во всех наследников поле id и атрибут
__tablename__ который заполняется автоматически. Имя таблицы берется из
названия класса и переводится в нижний регистр. Таблица наследник имеет по
умолчанию название и поле id, сильно облегчая жизнь.
"""
@declared_attr
def __tablename__(cls):
if (has_inherited_table(cls) and
Tablename not in cls.__bases__):
return None
return cls.__name__.lower()
# Method "byId" for use in code like this:
# session.query(Table).byId(5)
#
# SQL statement like:
# SELECT * FROM Table WHERE id = 5;
@classmethod
def byId(cls, id) :
return Session.query(cls).filter_by(id = id).first()
id = Column(Integer, autoincrement=True, primary_key=True)
class CreatedMixin(object):
"""Абстрактная примесь которая добавляет в другие модели поля:
created_by - кто создал
updated_by - последний кто обновил
created_at - дата создания
updated_at - дата последнего обновления
Поля заполняются автоматически. Кто создал и обновил ссылаются на модель
auth.User. Пользователь берется из текущей сессии, при помощи библиотеки
lib.auth и метода get_user
"""
@declared_attr
def created_by(cls):
return Column(Integer, ForeignKey('user.user_id',
onupdate="cascade", ondelete="restrict"))
@declared_attr
def updated_by(cls):
return Column(Integer, ForeignKey('user.user_id',
onupdate="cascade", ondelete="restrict"))
created_at = Column(DateTime, nullable=False, default=dt.now())
updated_at = Column(DateTime, nullable=False, default=dt.now(),
onupdate=dt.now())
Для полной картины приведу аналогичный пример на Django + DjangoORM.
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class ExtendedModel(models.Model):
created_by = models.ForeignKey(User, null=True, blank=True,
editable=False, related_name='%(class)s_creator')
created_time = models.DateTimeField(auto_now_add=True, editable=False)
modified_by = models.ForeignKey(User, null=True, blank=True,
editable=False, related_name='%(class)s_modifier')
modified_time = models.DateTimeField(auto_now=True, editable=False)
class Meta:
abstract = True
В папке accompaniment я привык держать всякие такие хелперы для проекта. Теперь
используем эту модель в нашем проекте myapp/projectname/models.py
:
from django.db import models
from accompaniment.models import ExtendedModel
class Ticket(ExtendedModel):
OPEN_STATUS = 1
REOPENED_STATUS = 2
RESOLVED_STATUS = 3
CLOSED_STATUS = 4
DUPLICATE_STATUS = 5
STATUS_CHOICES = (
(OPEN_STATUS, _('Open')),
(REOPENED_STATUS, _('Reopened')),
(RESOLVED_STATUS, _('Resolved')),
(CLOSED_STATUS, _('Closed')),
(DUPLICATE_STATUS, _('Duplicate')),
)
PRIORITY_CHOICES = (
(1, _('1. Critical')),
(2, _('2. High')),
(3, _('3. Normal')),
(4, _('4. Low')),
(5, _('5. Very Low')),
)
title = models.CharField(
_('Title'),
max_length=200,
)
queue = models.ForeignKey(
Queue,
verbose_name=_('Queue'),
)
assigned_to = models.ForeignKey(
User,
related_name='assigned_to',
blank=True,
null=True,
verbose_name=_('Assigned to'),
)
status = models.IntegerField(
_('Status'),
choices=STATUS_CHOICES,
default=OPEN_STATUS,
)
description = models.TextField(
_('Description'),
blank=True,
null=True,
help_text=_('The content of the customers query.'),
)
priority = models.IntegerField(
_('Priority'),
choices=PRIORITY_CHOICES,
default=3,
blank=3,
)
class Meta:
get_latest_by = "created"
verbose_name = u'Заявки'
verbose_name_plural = u'Заявки'
def __unicode__(self):
return u'%s' % self.title
def save(self, force_insert=False, force_update=False):
if not self.priority:
self.priority = 3
super(Ticket, self).save(force_insert, force_update)
Такой несложный метод освобождает нас от размножения кучи одинаковых полей в моделях.
Update: в моделях Mixin был атрибут __abstract__ = True
это неправильно,
так-как все таблицы стают абстрактными. НО! Это вполне прокатит на версиях
меньше 0.7, там этот атрибут почему-то не учитывается. На 0.7 версии работает
как надо. Вот описание проблемы: stackoverflow