You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

234 lines
9.2 KiB

'''
Associable Elixir Statement Generator
==========
Associable
==========
About Polymorphic Associations
------------------------------
A frequent pattern in database schemas is the has_and_belongs_to_many, or a
many-to-many table. Quite often multiple tables will refer to a single one
creating quite a few many-to-many intermediate tables.
Polymorphic associations lower the amount of many-to-many tables by setting up
a table that allows relations to any other table in the database, and relates
it to the associable table. In some implementations, this layout does not
enforce referential integrity with database foreign key constraints, this
implementation uses an additional many-to-many table with foreign key
constraints to avoid this problem.
.. note:
SQLite does not support foreign key constraints, so referential integrity
can only be enforced using database backends with such support.
Elixir Statement Generator for Polymorphic Associations
-------------------------------------------------------
The ``associable`` function generates the intermediary tables for an Elixir
entity that should be associable with other Elixir entities and returns an
Elixir Statement for use with them. This automates the process of creating the
polymorphic association tables and ensuring their referential integrity.
Matching select_XXX and select_by_XXX are also added to the associated entity
which allow queries to be run for the associated objects.
Example usage:
.. sourcecode:: python
class Tag(Entity):
name = Field(Unicode)
acts_as_taggable = associable(Tag)
class Entry(Entity):
title = Field(Unicode)
acts_as_taggable('tags')
class Article(Entity):
title = Field(Unicode)
acts_as_taggable('tags')
Or if one of the entities being associated should only have a single member of
the associated table:
.. sourcecode:: python
class Address(Entity):
street = Field(String(130))
city = Field(String(100))
is_addressable = associable(Address, 'addresses')
class Person(Entity):
name = Field(Unicode)
orders = OneToMany('Order')
is_addressable()
class Order(Entity):
order_num = Field(primary_key=True)
item_count = Field(Integer)
person = ManyToOne('Person')
is_addressable('address', uselist=False)
home = Address(street='123 Elm St.', city='Spooksville')
user = Person(name='Jane Doe')
user.addresses.append(home)
neworder = Order(item_count=4)
neworder.address = home
user.orders.append(neworder)
# Queries using the added helpers
Person.select_by_addresses(city='Cupertino')
Person.select_addresses(and_(Address.c.street=='132 Elm St',
Address.c.city=='Smallville'))
Statement Options
-----------------
The generated Elixir Statement has several options available:
+---------------+-------------------------------------------------------------+
| Option Name | Description |
+===============+=============================================================+
| ``name`` | Specify a custom name for the Entity attribute. This is |
| | used to declare the attribute used to access the associated |
| | table values. Otherwise, the name will use the plural_name |
| | provided to the associable call. |
+---------------+-------------------------------------------------------------+
| ``uselist`` | Whether or not the associated table should be represented |
| | as a list, or a single property. It should be set to False |
| | when the entity should only have a single associated |
| | entity. Defaults to True. |
+---------------+-------------------------------------------------------------+
| ``lazy`` | Determines eager loading of the associated entity objects. |
| | Defaults to False, to indicate that they should not be |
| | lazily loaded. |
+---------------+-------------------------------------------------------------+
'''
from elixir.statements import Statement
import sqlalchemy as sa
__doc_all__ = ['associable']
def associable(assoc_entity, plural_name=None, lazy=True):
'''
Generate an associable Elixir Statement
'''
interface_name = assoc_entity._descriptor.tablename
able_name = interface_name + 'able'
if plural_name:
attr_name = "%s_rel" % plural_name
else:
plural_name = interface_name
attr_name = "%s_rel" % interface_name
class GenericAssoc(object):
def __init__(self, tablename):
self.type = tablename
#TODO: inherit from entity builder
class Associable(object):
"""An associable Elixir Statement object"""
def __init__(self, entity, name=None, uselist=True, lazy=True):
self.entity = entity
self.lazy = lazy
self.uselist = uselist
if name is None:
self.name = plural_name
else:
self.name = name
def after_table(self):
col = sa.Column('%s_assoc_id' % interface_name, sa.Integer,
sa.ForeignKey('%s.id' % able_name))
self.entity._descriptor.add_column(col)
if not hasattr(assoc_entity, '_assoc_table'):
metadata = assoc_entity._descriptor.metadata
association_table = sa.Table("%s" % able_name, metadata,
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('type', sa.String(40), nullable=False),
)
tablename = "%s_to_%s" % (able_name, interface_name)
association_to_table = sa.Table(tablename, metadata,
sa.Column('assoc_id', sa.Integer,
sa.ForeignKey(association_table.c.id,
ondelete="CASCADE"),
primary_key=True),
#FIXME: this assumes a single id col
sa.Column('%s_id' % interface_name, sa.Integer,
sa.ForeignKey(assoc_entity.table.c.id,
ondelete="RESTRICT"),
primary_key=True),
)
assoc_entity._assoc_table = association_table
assoc_entity._assoc_to_table = association_to_table
def after_mapper(self):
if not hasattr(assoc_entity, '_assoc_mapper'):
assoc_entity._assoc_mapper = sa.orm.mapper(
GenericAssoc, assoc_entity._assoc_table, properties={
'targets': sa.orm.relation(
assoc_entity,
secondary=assoc_entity._assoc_to_table,
lazy=lazy, backref='associations',
order_by=assoc_entity.mapper.order_by)
})
entity = self.entity
entity.mapper.add_property(
attr_name,
sa.orm.relation(GenericAssoc, lazy=self.lazy,
backref='_backref_%s' % entity.table.name)
)
if self.uselist:
def get(self):
if getattr(self, attr_name) is None:
setattr(self, attr_name,
GenericAssoc(entity.table.name))
return getattr(self, attr_name).targets
setattr(entity, self.name, property(get))
else:
# scalar based property decorator
def get(self):
attr = getattr(self, attr_name)
if attr is not None:
return attr.targets[0]
else:
return None
def set(self, value):
if getattr(self, attr_name) is None:
setattr(self, attr_name,
GenericAssoc(entity.table.name))
getattr(self, attr_name).targets = [value]
setattr(entity, self.name, property(get, set))
# self.name is both set via mapper synonym and the python
# property, but that's how synonym properties work.
# adding synonym property after "real" property otherwise it
# breaks when using SQLAlchemy > 0.4.1
entity.mapper.add_property(self.name, sa.orm.synonym(attr_name))
# add helper methods
def select_by(cls, **kwargs):
return cls.query.join([attr_name, 'targets']) \
.filter_by(**kwargs).all()
setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
def select(cls, *args, **kwargs):
return cls.query.join([attr_name, 'targets']) \
.filter(*args, **kwargs).all()
setattr(entity, 'select_%s' % self.name, classmethod(select))
return Statement(Associable)