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
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)
|
|
|