Tags com o SQLAlchemy

Tradução de: http://pieceofpy.com/index.php/2008/10/09/tags-with-sqlalchemy/

Existem diversos artigos da Net sobre SQLalchemy. Implementação de um blog, um wiki, mesmo outros artigos sobre a implementação de tags. Alguns são bons, outros bem pobres, e alguns apenas estão desatualizados. Após alguma pesquisa sobre as melhores práticas sobre como implementar um sistema de Tags com o SQLalchemy eu cheguei à solução que você está prestes a ler.

Eu extraí esse exemplos de um código real em produção. Apenas renomeei e resumi um pouco para esse post. Peguei a convenção de nomes do exemplo SimpleSite do Pylons. Aqui está o layout das tabelas. Simples. Uma página, tag, e tabela de relação.

page_table = sa.Table("page", meta.metadata,
    sa.Column("id", sa.types.Integer, sa.schema.Sequence('page_seq_id', optional=True), primary_key=True),
    sa.Column("name", sa.types.Unicode(100), nullable=False),
)

tag_table = sa.Table("tag", meta.metadata,
    sa.Column("id", sa.types.Integer, sa.schema.Sequence('taq_seq_id', optional=True), primary_key=True),
    sa.Column("name", sa.types.Unicode(50), nullable=False, unique=True),
)

pagetag_table = sa.Table("pagetag", meta.metadata,
    sa.Column("id", sa.types.Integer, sa.schema.Sequence('pagetag_seq_id', optional=True), primary_key=True),
    sa.Column("pageid", sa.types.Integer, sa.schema.ForeignKey('page.id')),
    sa.Column("tagid", sa.types.Integer, sa.schema.ForeignKey('tag.id')),
)

Agora, a parte importente, o mapeamento. O mapeador é o que diz ao sqlalchemy o que você está tentando fazer e como relacionar essas ForeignKeys. Ele faz o trabalho pesado por você.

class Tag(object):
    pass

class Page(object):
    pass

orm.mapper(Tag, tag_table)
orm.mapper(Page, page_table, properties = {
    'tags':orm.relation(Tag, secondary=pagetag_table, cascade="all,delete-orphan"),
})

Isso faz duas coisas. Configura o relacionamento e também usa a regra built-in de cascata do SQLalchemy para garantir que não existam tags órfãs no banco de dados.

Agora podemos usar o modelo que configuramos. Aqui, eu apenas iniciei meu paster shell para que eu pudesse trabalhar alguns exemplos de uso.

page = model.Page()
page.name = "Example Page"

tag = model.Tag()
name = "tag"

page.tags.append(tag)
meta.Session.save(page)
meta.Session.commit()

tag_q = meta.Session.query(model.Tag)
tags = tag_q.all()
len(tags)

# filter pages by tag(s)
page_q = meta.Session.query(model.Page)
pages = page_q.join('tags').filter_by(name="tag").all()

# delete-orphans does the work for us here...
meta.Session.delete(pages[0])
meta.Session.commit()

tags = tag_q.all()
len(tags)

# tag cloud anyone?
# see the source code linked below for a properly weighted tag cloud.
tag_q = meta.Session.query(func.count("*").label("tagcount"), model.Tag)
tag_r = tag_q.filter(model.Tag.id==model.pagetag_table.c.tagid).group_by(model.Tag.id).all()

# what about pages with related tags?
page_q = meta.Session.query(model.Page)

taglist = ["tag1", "tag2"]
tagcount = len(taglist)
page_q.join(model.Page.tags).filter(model.Tag.name.in_(taglist)).\
group_by(model.Page.id).having(func.count(model.Page.id) == tagcount).all()

Ok, agora a parte divertida, e sobre todas as tags relacionadas? Uma intersecção entre um número arbitrário de relacionamentos muitos para muitos? Para isso eu adicionei um método estático a minha classe. Algo assim:

class Tag(object):
    @staticmethod
    def get_related(tags=[]):
        tag_count = len(tags)

        inner_q = select([pagetag_table.c.pageid])
        inner_w = inner_q.where(
            and_(pagetag_table.c.tagid == Tag.id,Tag.name.in_(tags))
        ).group_by(pagetag_table.c.pageid).having(func.count(pagetag_table.c.pageid) == tag_count).correlate(None)

        outer_q = select([Tag.id, Tag.name, func.count(pagetag_table.c.shipid)])
        outer_w = outer_q.where(
            and_(pagetag_table.c.pageid.in_(inner_w),
            not_(Tag.name.in_(tags)),
            Tag.id == pagetag_table.c.tagid)
        ).group_by(pagetag_table.c.tagid)

        related_tags = meta.Session.execute(outer_w).fetchall()
        return related_tags

Um grande agradecimento ao CakePHP e ao TagSchema pelas idéias, conceitos e exemplos de implementação.

Você pode encontrar o código real no qual esse texto foi baseado em: http://trac.pieceofpy.com/pieceofpy/browser/tags-sqlalchemy

# % if c.boo_box: DVD Um dia de Cão DVD O Álibi Perfeito DVD Protegida por um Anjo Access XP: Plus Resumão: Access 2000 Forma de Gelo Silicone Formato Dados - Amarela