mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-30 12:34:52 -04:00
- SqlSoup overhaul
- Added "map_to()" method to SqlSoup, which is a "master" method which accepts explicit arguments for each aspect of the selectable and mapping, including a base class per mapping. [ticket:1975] - Mapped selectables used with the map(), with_labels(), join() methods no longer put the given argument into the internal "cache" dictionary. Particularly since the join() and select() objects are created in the method itself this was pretty much a pure memory leaking behavior.
This commit is contained in:
@@ -98,6 +98,18 @@ CHANGES
|
||||
- declarative
|
||||
- An error is raised if __table_args__ is not in tuple
|
||||
or dict format, and is not None. [ticket:1972]
|
||||
|
||||
- sqlsoup
|
||||
- Added "map_to()" method to SqlSoup, which is a "master"
|
||||
method which accepts explicit arguments for each aspect of
|
||||
the selectable and mapping, including a base class per
|
||||
mapping. [ticket:1975]
|
||||
|
||||
- Mapped selectables used with the map(), with_labels(),
|
||||
join() methods no longer put the given argument into the
|
||||
internal "cache" dictionary. Particularly since the
|
||||
join() and select() objects are created in the method
|
||||
itself this was pretty much a pure memory leaking behavior.
|
||||
|
||||
0.6.5
|
||||
=====
|
||||
|
||||
Vendored
+6
-1
@@ -2,5 +2,10 @@ SqlSoup
|
||||
=======
|
||||
|
||||
.. automodule:: sqlalchemy.ext.sqlsoup
|
||||
|
||||
|
||||
SqlSoup API
|
||||
------------
|
||||
|
||||
.. autoclass:: sqlalchemy.ext.sqlsoup.SqlSoup
|
||||
:members:
|
||||
|
||||
|
||||
+355
-129
@@ -2,19 +2,20 @@
|
||||
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 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 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::
|
||||
@@ -39,53 +40,73 @@ Loading objects is as easy as this::
|
||||
>>> 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
|
||||
=================
|
||||
@@ -96,12 +117,12 @@ Modifying objects is intuitive::
|
||||
>>> 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)
|
||||
@@ -111,14 +132,12 @@ it::
|
||||
>>> 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)
|
||||
@@ -129,7 +148,8 @@ book_id to 1 in all loans whose book_id is 2::
|
||||
|
||||
>>> 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
|
||||
@@ -140,13 +160,15 @@ tables all at once. In this situation, it is far more efficient to
|
||||
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,
|
||||
@@ -162,20 +184,29 @@ books table::
|
||||
|
||||
>>> 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
|
||||
@@ -188,13 +219,16 @@ You can define relationships on SqlSoup classes:
|
||||
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')
|
||||
@@ -205,38 +239,47 @@ Advanced Use
|
||||
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.
|
||||
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.
|
||||
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 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)))
|
||||
@@ -245,15 +288,13 @@ closes out its transaction immediately and no external management is needed. ``
|
||||
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
|
||||
@@ -276,44 +317,45 @@ your db object::
|
||||
|
||||
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,\
|
||||
@@ -403,7 +445,7 @@ def _selectable_name(selectable):
|
||||
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
|
||||
@@ -459,16 +501,25 @@ def _class_for_table(session, engine, selectable, base_cls=object, **mapper_kwar
|
||||
return klass
|
||||
|
||||
class SqlSoup(object):
|
||||
def __init__(self, engine_or_metadata, base=object, **kw):
|
||||
"""Initialize a new ``SqlSoup``.
|
||||
"""Represent an ORM-wrapped database resource."""
|
||||
|
||||
def __init__(self, engine_or_metadata, base=object, session=None):
|
||||
"""Initialize a new :class:`.SqlSoup`.
|
||||
|
||||
`base` is the class that all created entity classes should subclass.
|
||||
: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):
|
||||
@@ -476,21 +527,32 @@ class SqlSoup(object):
|
||||
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
|
||||
@@ -501,58 +563,222 @@ class SqlSoup(object):
|
||||
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")
|
||||
|
||||
def with_labels(self, item):
|
||||
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`.
|
||||
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from sqlalchemy.ext import sqlsoup
|
||||
from sqlalchemy.test.testing import TestBase, eq_, assert_raises
|
||||
from sqlalchemy.test.testing import TestBase, eq_, assert_raises, \
|
||||
assert_raises_message
|
||||
from sqlalchemy import create_engine, or_, desc, select, func, exc, \
|
||||
Table, util
|
||||
Table, util, Column, Integer
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
import datetime
|
||||
|
||||
@@ -30,6 +31,76 @@ class SQLSoupTest(TestBase):
|
||||
for sql in _teardown:
|
||||
engine.execute(sql)
|
||||
|
||||
def test_map_to_attr_present(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
users = db.users
|
||||
assert_raises_message(
|
||||
exc.InvalidRequestError,
|
||||
"Attribute 'users' is already mapped",
|
||||
db.map_to, 'users', tablename='users'
|
||||
)
|
||||
|
||||
def test_map_to_table_not_string(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
table = Table('users', db._metadata, Column('id', Integer, primary_key=True))
|
||||
assert_raises_message(
|
||||
exc.ArgumentError,
|
||||
"'tablename' argument must be a string.",
|
||||
db.map_to, 'users', tablename=table
|
||||
)
|
||||
|
||||
def test_map_to_table_or_selectable(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
table = Table('users', db._metadata, Column('id', Integer, primary_key=True))
|
||||
assert_raises_message(
|
||||
exc.ArgumentError,
|
||||
"'tablename' and 'selectable' arguments are mutually exclusive",
|
||||
db.map_to, 'users', tablename='users', selectable=table
|
||||
)
|
||||
|
||||
def test_map_to_no_pk_selectable(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
table = Table('users', db._metadata, Column('id', Integer))
|
||||
assert_raises_message(
|
||||
sqlsoup.PKNotFoundError,
|
||||
"table 'users' does not have a primary ",
|
||||
db.map_to, 'users', selectable=table
|
||||
)
|
||||
def test_map_to_invalid_schema(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
table = Table('users', db._metadata, Column('id', Integer))
|
||||
assert_raises_message(
|
||||
exc.ArgumentError,
|
||||
"'tablename' argument is required when "
|
||||
"using 'schema'.",
|
||||
db.map_to, 'users', selectable=table, schema='hoho'
|
||||
)
|
||||
def test_map_to_nothing(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
assert_raises_message(
|
||||
exc.ArgumentError,
|
||||
"'tablename' or 'selectable' argument is "
|
||||
"required.",
|
||||
db.map_to, 'users',
|
||||
)
|
||||
|
||||
def test_map_to_string_not_selectable(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
assert_raises_message(
|
||||
exc.ArgumentError,
|
||||
"'selectable' argument must be a "
|
||||
"table, select, join, or other "
|
||||
"selectable construct.",
|
||||
db.map_to, 'users', selectable='users'
|
||||
)
|
||||
|
||||
def test_bad_names(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
|
||||
@@ -278,7 +349,7 @@ class SQLSoupTest(TestBase):
|
||||
email=u'student@example.edu', password=u'student',
|
||||
classname=None, admin=0)])
|
||||
|
||||
def test_no_pk(self):
|
||||
def test_no_pk_reflected(self):
|
||||
db = sqlsoup.SqlSoup(engine)
|
||||
assert_raises(sqlsoup.PKNotFoundError, getattr, db, 'nopk')
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import testenv; testenv.simple_setup()
|
||||
import sys, time
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import *
|
||||
@@ -87,7 +86,7 @@ def all():
|
||||
|
||||
run_profiled(sa_profiled_insert_many,
|
||||
'SQLAlchemy bulk insert/select, profiled',
|
||||
1000)
|
||||
50000)
|
||||
|
||||
print "\nIndividual INSERTS via execute():\n"
|
||||
|
||||
@@ -101,7 +100,7 @@ def all():
|
||||
|
||||
run_profiled(sa_profiled_insert,
|
||||
'SQLAlchemy individual insert/select, profiled',
|
||||
1000)
|
||||
50000)
|
||||
|
||||
finally:
|
||||
metadata.drop_all()
|
||||
|
||||
Reference in New Issue
Block a user