Introduction
============
-SqlSoup provides a convenient way to access existing database tables without
-having to declare table or mapper classes ahead of time. It is built on top of
-the SQLAlchemy ORM and provides a super-minimalistic interface to an existing
-database.
-
-SqlSoup effectively provides a coarse grained, alternative interface to
-working with the SQLAlchemy ORM, providing a "self configuring" interface
-for extremely rudimental operations. It's somewhat akin to a
-"super novice mode" version of the ORM. While SqlSoup can be very handy,
-users are strongly encouraged to use the full ORM for non-trivial applications.
+SqlSoup provides a convenient way to access existing database
+tables without having to declare table or mapper classes ahead
+of time. It is built on top of the SQLAlchemy ORM and provides a
+super-minimalistic interface to an existing database.
+
+SqlSoup effectively provides a coarse grained, alternative
+interface to working with the SQLAlchemy ORM, providing a "self
+configuring" interface for extremely rudimental operations. It's
+somewhat akin to a "super novice mode" version of the ORM. While
+SqlSoup can be very handy, users are strongly encouraged to use
+the full ORM for non-trivial applications.
Suppose we have a database with users, books, and loans tables
-(corresponding to the PyWebOff dataset, if you're curious).
+(corresponding to the PyWebOff dataset, if you're curious).
Creating a SqlSoup gateway is just like creating an SQLAlchemy
engine::
>>> users = db.users.all()
>>> users.sort()
>>> users
- [MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0), MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)]
+ [
+ MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+ password=u'student',classname=None,admin=0),
+ MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+ password=u'basepair',classname=None,admin=1)
+ ]
Of course, letting the database do the sort is better::
>>> db.users.order_by(db.users.name).all()
- [MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1), MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0)]
+ [
+ MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+ password=u'basepair',classname=None,admin=1),
+ MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+ password=u'student',classname=None,admin=0)
+ ]
Field access is intuitive::
>>> users[0].email
u'student@example.edu'
-Of course, you don't want to load all users very often. Let's add a
-WHERE clause. Let's also switch the order_by to DESC while we're at
-it::
+Of course, you don't want to load all users very often. Let's
+add a WHERE clause. Let's also switch the order_by to DESC while
+we're at it::
>>> from sqlalchemy import or_, and_, desc
>>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu')
>>> db.users.filter(where).order_by(desc(db.users.name)).all()
- [MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0), MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)]
+ [
+ MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+ password=u'student',classname=None,admin=0),
+ MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+ password=u'basepair',classname=None,admin=1)
+ ]
-You can also use .first() (to retrieve only the first object from a query) or
-.one() (like .first when you expect exactly one user -- it will raise an
-exception if more were returned)::
+You can also use .first() (to retrieve only the first object
+from a query) or .one() (like .first when you expect exactly one
+user -- it will raise an exception if more were returned)::
>>> db.users.filter(db.users.name=='Bhargan Basepair').one()
- MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
+ MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+ password=u'basepair',classname=None,admin=1)
Since name is the primary key, this is equivalent to
>>> db.users.get('Bhargan Basepair')
- MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
+ MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+ password=u'basepair',classname=None,admin=1)
This is also equivalent to
>>> db.users.filter_by(name='Bhargan Basepair').one()
- MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
+ MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+ password=u'basepair',classname=None,admin=1)
-filter_by is like filter, but takes kwargs instead of full clause expressions.
-This makes it more concise for simple queries like this, but you can't do
-complex queries like the or\_ above or non-equality based comparisons this way.
+filter_by is like filter, but takes kwargs instead of full
+clause expressions. This makes it more concise for simple
+queries like this, but you can't do complex queries like the
+or\_ above or non-equality based comparisons this way.
Full query documentation
------------------------
Get, filter, filter_by, order_by, limit, and the rest of the
-query methods are explained in detail in :ref:`ormtutorial_querying`.
+query methods are explained in detail in
+:ref:`ormtutorial_querying`.
Modifying objects
=================
>>> user.email = 'basepair+nospam@example.edu'
>>> db.commit()
-(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work code, so
-multiple updates to a single object will be turned into a single
-``UPDATE`` statement when you commit.)
+(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work
+code, so multiple updates to a single object will be turned into
+a single ``UPDATE`` statement when you commit.)
-To finish covering the basics, let's insert a new loan, then delete
-it::
+To finish covering the basics, let's insert a new loan, then
+delete it::
>>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id
>>> db.loans.insert(book_id=book_id, user_name=user.name)
>>> db.delete(loan)
>>> db.commit()
-You can also delete rows that have not been loaded as objects. Let's
-do our insert/delete cycle once more, this time using the loans
-table's delete method. (For SQLAlchemy experts: note that no flush()
-call is required since this delete acts at the SQL level, not at the
-Mapper level.) The same where-clause construction rules apply here as
-to the select methods.
-
-::
+You can also delete rows that have not been loaded as objects.
+Let's do our insert/delete cycle once more, this time using the
+loans table's delete method. (For SQLAlchemy experts: note that
+no flush() call is required since this delete acts at the SQL
+level, not at the Mapper level.) The same where-clause
+construction rules apply here as to the select methods::
>>> db.loans.insert(book_id=book_id, user_name=user.name)
MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
>>> db.loans.update(db.loans.book_id==2, book_id=1)
>>> db.loans.filter_by(book_id=1).all()
- [MappedLoans(book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+ [MappedLoans(book_id=1,user_name=u'Joe Student',
+ loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
Joins
have the database perform the necessary join. (Here we do not have *a
lot of data* but hopefully the concept is still clear.) SQLAlchemy is
smart enough to recognize that loans has a foreign key to users, and
-uses that as the join condition automatically.
-
-::
+uses that as the join condition automatically::
>>> join1 = db.join(db.users, db.loans, isouter=True)
>>> join1.filter_by(name='Joe Student').all()
- [MappedJoin(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0,book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+ [
+ MappedJoin(name=u'Joe Student',email=u'student@example.edu',
+ password=u'student',classname=None,admin=0,book_id=1,
+ user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))
+ ]
If you're unfortunate enough to be using MySQL with the default MyISAM
storage engine, you'll have to specify the join condition manually,
>>> join2 = db.join(join1, db.books)
>>> join2.all()
- [MappedJoin(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0,book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),id=1,title=u'Mustards I Have Known',published_year=u'1989',authors=u'Jones')]
+ [
+ MappedJoin(name=u'Joe Student',email=u'student@example.edu',
+ password=u'student',classname=None,admin=0,book_id=1,
+ user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),
+ id=1,title=u'Mustards I Have Known',published_year=u'1989',
+ authors=u'Jones')
+ ]
If you join tables that have an identical column name, wrap your join
with `with_labels`, to disambiguate columns with their table name
(.c is short for .columns)::
>>> db.with_labels(join1).c.keys()
- [u'users_name', u'users_email', u'users_password', u'users_classname', u'users_admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
+ [u'users_name', u'users_email', u'users_password',
+ u'users_classname', u'users_admin', u'loans_book_id',
+ u'loans_user_name', u'loans_loan_date']
You can also join directly to a labeled object::
>>> labeled_loans = db.with_labels(db.loans)
>>> db.join(db.users, labeled_loans, isouter=True).c.keys()
- [u'name', u'email', u'password', u'classname', u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
+ [u'name', u'email', u'password', u'classname',
+ u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
Relationships
These can then be used like a normal SA property:
>>> db.users.get('Joe Student').loans
- [MappedLoans(book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+ [MappedLoans(book_id=1,user_name=u'Joe Student',
+ loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
>>> db.users.filter(~db.users.loans.any()).all()
- [MappedUsers(name=u'Bhargan Basepair',email='basepair+nospam@example.edu',password=u'basepair',classname=None,admin=1)]
-
+ [MappedUsers(name=u'Bhargan Basepair',
+ email='basepair+nospam@example.edu',
+ password=u'basepair',classname=None,admin=1)]
-relate can take any options that the relationship function accepts in normal mapper definition:
+relate can take any options that the relationship function
+accepts in normal mapper definition:
>>> del db._cache['users']
>>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
Sessions, Transations and Application Integration
-------------------------------------------------
-**Note:** please read and understand this section thoroughly before using SqlSoup in any web application.
+**Note:** please read and understand this section thoroughly
+before using SqlSoup in any web application.
-SqlSoup uses a ScopedSession to provide thread-local sessions. You
-can get a reference to the current one like this::
+SqlSoup uses a ScopedSession to provide thread-local sessions.
+You can get a reference to the current one like this::
>>> session = db.session
-The default session is available at the module level in SQLSoup, via::
+The default session is available at the module level in SQLSoup,
+via::
>>> from sqlalchemy.ext.sqlsoup import Session
-The configuration of this session is ``autoflush=True``, ``autocommit=False``.
-This means when you work with the SqlSoup object, you need to call ``db.commit()``
-in order to have changes persisted. You may also call ``db.rollback()`` to
-roll things back.
-
-Since the SqlSoup object's Session automatically enters into a transaction as soon
-as it's used, it is *essential* that you call ``commit()`` or ``rollback()``
-on it when the work within a thread completes. This means all the guidelines
-for web application integration at :ref:`session_lifespan` must be followed.
-
-The SqlSoup object can have any session or scoped session configured onto it.
-This is of key importance when integrating with existing code or frameworks
-such as Pylons. If your application already has a ``Session`` configured,
-pass it to your SqlSoup object::
+The configuration of this session is ``autoflush=True``,
+``autocommit=False``. This means when you work with the SqlSoup
+object, you need to call ``db.commit()`` in order to have
+changes persisted. You may also call ``db.rollback()`` to roll
+things back.
+
+Since the SqlSoup object's Session automatically enters into a
+transaction as soon as it's used, it is *essential* that you
+call ``commit()`` or ``rollback()`` on it when the work within a
+thread completes. This means all the guidelines for web
+application integration at :ref:`session_lifespan` must be
+followed.
+
+The SqlSoup object can have any session or scoped session
+configured onto it. This is of key importance when integrating
+with existing code or frameworks such as Pylons. If your
+application already has a ``Session`` configured, pass it to
+your SqlSoup object::
>>> from myapplication import Session
>>> db = SqlSoup(session=Session)
-If the ``Session`` is configured with ``autocommit=True``, use ``flush()``
-instead of ``commit()`` to persist changes - in this case, the ``Session``
-closes out its transaction immediately and no external management is needed. ``rollback()`` is also not available. Configuring a new SQLSoup object in "autocommit" mode looks like::
+If the ``Session`` is configured with ``autocommit=True``, use
+``flush()`` instead of ``commit()`` to persist changes - in this
+case, the ``Session`` closes out its transaction immediately and
+no external management is needed. ``rollback()`` is also not
+available. Configuring a new SQLSoup object in "autocommit" mode
+looks like::
>>> from sqlalchemy.orm import scoped_session, sessionmaker
>>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
Mapping arbitrary Selectables
-----------------------------
-SqlSoup can map any SQLAlchemy ``Selectable`` with the map
-method. Let's map a ``Select`` object that uses an aggregate function;
-we'll use the SQLAlchemy ``Table`` that SqlSoup introspected as the
-basis. (Since we're not mapping to a simple table or join, we need to
-tell SQLAlchemy how to find the *primary key* which just needs to be
-unique within the select, and not necessarily correspond to a *real*
-PK in the database.)
-
-::
+SqlSoup can map any SQLAlchemy :class:`.Selectable` with the map
+method. Let's map an :func:`.expression.select` object that uses an aggregate
+function; we'll use the SQLAlchemy :class:`.Table` that SqlSoup
+introspected as the basis. (Since we're not mapping to a simple
+table or join, we need to tell SQLAlchemy how to find the
+*primary key* which just needs to be unique within the select,
+and not necessarily correspond to a *real* PK in the database.)::
>>> from sqlalchemy import select, func
>>> b = db.books._table
Python is flexible like that!
-
Raw SQL
-------
-SqlSoup works fine with SQLAlchemy's text construct, described in :ref:`sqlexpression_text`.
-You can also execute textual SQL directly using the `execute()` method,
-which corresponds to the `execute()` method on the underlying `Session`.
-Expressions here are expressed like ``text()`` constructs, using named parameters
+SqlSoup works fine with SQLAlchemy's text construct, described
+in :ref:`sqlexpression_text`. You can also execute textual SQL
+directly using the `execute()` method, which corresponds to the
+`execute()` method on the underlying `Session`. Expressions here
+are expressed like ``text()`` constructs, using named parameters
with colons::
>>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%')
>>> for name, email in rp.fetchall(): print name, email
Bhargan Basepair basepair+nospam@example.edu
-Or you can get at the current transaction's connection using `connection()`. This is the
-raw connection object which can accept any sort of SQL expression or raw SQL string passed to the database::
+Or you can get at the current transaction's connection using
+`connection()`. This is the raw connection object which can
+accept any sort of SQL expression or raw SQL string passed to
+the database::
>>> conn = db.connection()
>>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%')
-
Dynamic table names
-------------------
-You can load a table whose name is specified at runtime with the entity() method:
+You can load a table whose name is specified at runtime with the
+entity() method:
>>> tablename = 'loans'
>>> db.entity(tablename) == db.loans
True
-entity() also takes an optional schema argument. If none is specified, the
-default schema is used.
-
+entity() also takes an optional schema argument. If none is
+specified, the default schema is used.
"""
from sqlalchemy import Table, MetaData, join
-from sqlalchemy import schema, sql
+from sqlalchemy import schema, sql, util
from sqlalchemy.engine.base import Engine
from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
class_mapper, relationship, session,\
x = x[1:]
return x
-def _class_for_table(session, engine, selectable, base_cls=object, **mapper_kwargs):
+def _class_for_table(session, engine, selectable, base_cls, mapper_kwargs):
selectable = expression._clause_element_as_expr(selectable)
mapname = 'Mapped' + _selectable_name(selectable)
# Py2K
return klass
class SqlSoup(object):
- def __init__(self, engine_or_metadata, base=object, **kw):
- """Initialize a new ``SqlSoup``.
-
- `base` is the class that all created entity classes should subclass.
+ """Represent an ORM-wrapped database resource."""
+
+ def __init__(self, engine_or_metadata, base=object, session=None):
+ """Initialize a new :class:`.SqlSoup`.
+
+ :param engine_or_metadata: a string database URL, :class:`.Engine`
+ or :class:`.MetaData` object to associate with. If the
+ argument is a :class:`.MetaData`, it should be *bound*
+ to an :class:`.Engine`.
+ :param base: a class which will serve as the default class for
+ returned mapped classes. Defaults to ``object``.
+ :param session: a :class:`.ScopedSession` or :class:`.Session` with
+ which to associate ORM operations for this :class:`.SqlSoup` instance.
+ If ``None``, a :class:`.ScopedSession` that's local to this
+ module is used.
- `args` may either be an ``SQLEngine`` or a set of arguments
- suitable for passing to ``create_engine``.
"""
- self.session = kw.pop('session', Session)
+ self.session = session or Session
self.base=base
if isinstance(engine_or_metadata, MetaData):
elif isinstance(engine_or_metadata, (basestring, Engine)):
self._metadata = MetaData(engine_or_metadata)
else:
- raise ArgumentError("invalid engine or metadata argument %r" % engine_or_metadata)
+ raise ArgumentError("invalid engine or metadata argument %r" %
+ engine_or_metadata)
self._cache = {}
self.schema = None
@property
- def engine(self):
+ def bind(self):
+ """The :class:`.Engine` associated with this :class:`.SqlSoup`."""
return self._metadata.bind
- bind = engine
+ engine = bind
- def delete(self, *args, **kwargs):
- self.session.delete(*args, **kwargs)
+ def delete(self, instance):
+ """Mark an instance as deleted."""
+
+ self.session.delete(instance)
def execute(self, stmt, **params):
+ """Execute a SQL statement.
+
+ The statement may be a string SQL string,
+ an :func:`.expression.select` construct, or an :func:`.expression.text`
+ construct.
+
+ """
return self.session.execute(sql.text(stmt, bind=self.bind), **params)
@property
return self.session()
def connection(self):
+ """Return the current :class:`.Connection` in use by the current transaction."""
+
return self._underlying_session._connection_for_bind(self.bind)
def flush(self):
+ """Flush pending changes to the database.
+
+ See :meth:`.Session.flush`.
+
+ """
self.session.flush()
def rollback(self):
+ """Rollback the current transction.
+
+ See :meth:`.Session.rollback`.
+
+ """
self.session.rollback()
def commit(self):
+ """Commit the current transaction.
+
+ See :meth:`.Session.commit`.
+
+ """
self.session.commit()
def clear(self):
+ """Synonym for :meth:`.SqlSoup.expunge_all`."""
+
self.session.expunge_all()
- def expunge(self, *args, **kw):
- self.session.expunge(*args, **kw)
+ def expunge(self, instance):
+ """Remove an instance from the :class:`.Session`.
+
+ See :meth:`.Session.expunge`.
+
+ """
+ self.session.expunge(instance)
def expunge_all(self):
+ """Clear all objects from the current :class:`.Session`.
+
+ See :meth:`.Session.expunge_all`.
+
+ """
self.session.expunge_all()
- def map(self, selectable, **kwargs):
- try:
- t = self._cache[selectable]
- except KeyError:
- t = _class_for_table(self.session, self.engine, selectable, **kwargs)
- self._cache[selectable] = t
- return t
+ def map_to(self, attrname, tablename=None, selectable=None,
+ schema=None, base=None, mapper_args=util.frozendict()):
+ """Configure a mapping to the given attrname.
+
+ This is the "master" method that can be used to create any
+ configuration.
+
+ :param attrname: String attribute name which will be
+ established as an attribute on this :class:.`.SqlSoup`
+ instance.
+ :param base: a Python class which will be used as the
+ base for the mapped class. If ``None``, the "base"
+ argument specified by this :class:`.SqlSoup`
+ instance's constructor will be used, which defaults to
+ ``object``.
+ :param mapper_args: Dictionary of arguments which will
+ be passed directly to :func:`.orm.mapper`.
+ :param tablename: String name of a :class:`.Table` to be
+ reflected. If a :class:`.Table` is already available,
+ use the ``selectable`` argument. This argument is
+ mutually exclusive versus the ``selectable`` argument.
+ :param selectable: a :class:`.Table`, :class:`.Join`, or
+ :class:`.Select` object which will be mapped. This
+ argument is mutually exclusive versus the ``tablename``
+ argument.
+ :param schema: String schema name to use if the
+ ``tablename`` argument is present.
+
+
+ """
+ if attrname in self._cache:
+ raise InvalidRequestError(
+ "Attribute '%s' is already mapped to '%s'" % (
+ attrname,
+ class_mapper(self._cache[attrname]).mapped_table
+ ))
+
+ if tablename is not None:
+ if not isinstance(tablename, basestring):
+ raise ArgumentError("'tablename' argument must be a string."
+ )
+ if selectable is not None:
+ raise ArgumentError("'tablename' and 'selectable' "
+ "arguments are mutually exclusive")
+
+ selectable = Table(tablename,
+ self._metadata,
+ autoload=True,
+ autoload_with=self.bind,
+ schema=schema or self.schema)
+ elif schema:
+ raise ArgumentError("'tablename' argument is required when "
+ "using 'schema'.")
+ elif selectable is not None:
+ if not isinstance(selectable, expression.FromClause):
+ raise ArgumentError("'selectable' argument must be a "
+ "table, select, join, or other "
+ "selectable construct.")
+ else:
+ raise ArgumentError("'tablename' or 'selectable' argument is "
+ "required.")
+
+ if not selectable.primary_key.columns:
+ if tablename:
+ raise PKNotFoundError(
+ "table '%s' does not have a primary "
+ "key defined" % tablename)
+ else:
+ raise PKNotFoundError(
+ "selectable '%s' does not have a primary "
+ "key defined" % selectable)
+
+ mapped_cls = _class_for_table(
+ self.session,
+ self.engine,
+ selectable,
+ base or self.base,
+ mapper_args
+ )
+ self._cache[attrname] = mapped_cls
+ return mapped_cls
+
+
+ def map(self, selectable, base=None, **mapper_args):
+ """Map a selectable directly.
+
+ The class and its mapping are not cached and will
+ be discarded once dereferenced (as of 0.6.6).
+
+ :param selectable: an :func:`.expression.select` construct.
+ :param base: a Python class which will be used as the
+ base for the mapped class. If ``None``, the "base"
+ argument specified by this :class:`.SqlSoup`
+ instance's constructor will be used, which defaults to
+ ``object``.
+ :param mapper_args: Dictionary of arguments which will
+ be passed directly to :func:`.orm.mapper`.
+
+ """
- def with_labels(self, item):
+ return _class_for_table(
+ self.session,
+ self.engine,
+ selectable,
+ base or self.base,
+ mapper_args
+ )
+
+ def with_labels(self, selectable, base=None, **mapper_args):
+ """Map a selectable directly, wrapping the
+ selectable in a subquery with labels.
+
+ The class and its mapping are not cached and will
+ be discarded once dereferenced (as of 0.6.6).
+
+ :param selectable: an :func:`.expression.select` construct.
+ :param base: a Python class which will be used as the
+ base for the mapped class. If ``None``, the "base"
+ argument specified by this :class:`.SqlSoup`
+ instance's constructor will be used, which defaults to
+ ``object``.
+ :param mapper_args: Dictionary of arguments which will
+ be passed directly to :func:`.orm.mapper`.
+
+ """
+
# TODO give meaningful aliases
return self.map(
- expression._clause_element_as_expr(item).
+ expression._clause_element_as_expr(selectable).
select(use_labels=True).
- alias('foo'))
+ alias('foo'), base=base, **mapper_args)
- def join(self, *args, **kwargs):
- j = join(*args, **kwargs)
- return self.map(j)
+ def join(self, left, right, onclause=None, isouter=False,
+ base=None, **mapper_args):
+ """Create an :func:`.expression.join` and map to it.
+
+ The class and its mapping are not cached and will
+ be discarded once dereferenced (as of 0.6.6).
+
+ :param left: a mapped class or table object.
+ :param right: a mapped class or table object.
+ :param onclause: optional "ON" clause construct..
+ :param isouter: if True, the join will be an OUTER join.
+ :param base: a Python class which will be used as the
+ base for the mapped class. If ``None``, the "base"
+ argument specified by this :class:`.SqlSoup`
+ instance's constructor will be used, which defaults to
+ ``object``.
+ :param mapper_args: Dictionary of arguments which will
+ be passed directly to :func:`.orm.mapper`.
+
+ """
+
+ j = join(left, right, onclause=onclause, isouter=isouter)
+ return self.map(j, base=base, **mapper_args)
def entity(self, attr, schema=None):
+ """Return the named entity from this :class:`.SqlSoup`, or
+ create if not present.
+
+ For more generalized mapping, see :meth:`.map_to`.
+
+ """
try:
- t = self._cache[attr]
+ return self._cache[attr]
except KeyError, ke:
- table = Table(attr, self._metadata, autoload=True, autoload_with=self.bind, schema=schema or self.schema)
- if not table.primary_key.columns:
- raise PKNotFoundError('table %r does not have a primary key defined [columns: %s]' % (attr, ','.join(table.c.keys())))
- if table.columns:
- t = _class_for_table(self.session, self.engine, table, self.base)
- else:
- t = None
- self._cache[attr] = t
- return t
+ return self.map_to(attr, tablename=attr, schema=schema)
def __getattr__(self, attr):
return self.entity(attr)