Skip to content

Commit

Permalink
Improved session creation.
Browse files Browse the repository at this point in the history
This fixes pallets-eco#144
  • Loading branch information
mitsuhiko committed Jul 31, 2013
1 parent a2ea524 commit 575fa65
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 62 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Version 2.0
- Changed how the builtin signals are subscribed to skip non Flask-SQLAlchemy
sessions. This will also fix the attribute error about model changes
not existing.
- Added a way to control how signals for model modifications are tracked.
- Made the ``SignallingSession`` a public interface and added a hook
for customizing session creation.

Version 1.0
-----------
Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ Models
Return the first result of this query or `None` if the result
doesn’t contain any rows. This results in an execution of the
underlying query.

Sessions
````````

.. autoclass:: SignallingSession
:members:

Utilities
`````````
Expand Down
92 changes: 50 additions & 42 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,53 @@ A list of configuration keys currently understood by the extension:

.. tabularcolumns:: |p{6.5cm}|p{8.5cm}|

================================= =========================================
``SQLALCHEMY_DATABASE_URI`` The database URI that should be used for
the connection. Examples:

- ``sqlite:////tmp/test.db``
- ``mysql://username:password@server/db``
``SQLALCHEMY_BINDS`` A dictionary that maps bind keys to
SQLAlchemy connection URIs. For more
information about binds see :ref:`binds`.
``SQLALCHEMY_ECHO`` If set to `True` SQLAlchemy will log all
the statements issued to stderr which can
be useful for debugging.
``SQLALCHEMY_RECORD_QUERIES`` Can be used to explicitly disable or
enable query recording. Query recording
automatically happens in debug or testing
mode. See :func:`get_debug_queries` for
more information.
``SQLALCHEMY_NATIVE_UNICODE`` Can be used to explicitly disable native
unicode support. This is required for
some database adapters (like PostgreSQL
on some Ubuntu versions) when used with
inproper database defaults that specify
encoding-less databases.
``SQLALCHEMY_POOL_SIZE`` The size of the database pool. Defaults
to the engine's default (usually 5)
``SQLALCHEMY_POOL_TIMEOUT`` Specifies the connection timeout for the
pool. Defaults to 10.
``SQLALCHEMY_POOL_RECYCLE`` Number of seconds after which a
connection is automatically recycled.
This is required for MySQL, which removes
connections after 8 hours idle by
default. Note that Flask-SQLAlchemy
automatically sets this to 2 hours if
MySQL is used.
``SQLALCHEMY_MAX_OVERFLOW`` Controls the number of connections that
can be created after the pool reached
its maximum size. When those additional
connections are returned to the pool,
they are disconnected and discarded.
``SQLALCHEMY_COMMIT_ON_TEARDOWN`` Commit session when the app context is
torn down, unless there was an exception.
================================= =========================================
================================== =========================================
``SQLALCHEMY_DATABASE_URI`` The database URI that should be used for
the connection. Examples:

- ``sqlite:////tmp/test.db``
- ``mysql://username:password@server/db``
``SQLALCHEMY_BINDS`` A dictionary that maps bind keys to
SQLAlchemy connection URIs. For more
information about binds see :ref:`binds`.
``SQLALCHEMY_ECHO`` If set to `True` SQLAlchemy will log all
the statements issued to stderr which can
be useful for debugging.
``SQLALCHEMY_RECORD_QUERIES`` Can be used to explicitly disable or
enable query recording. Query recording
automatically happens in debug or testing
mode. See :func:`get_debug_queries` for
more information.
``SQLALCHEMY_NATIVE_UNICODE`` Can be used to explicitly disable native
unicode support. This is required for
some database adapters (like PostgreSQL
on some Ubuntu versions) when used with
inproper database defaults that specify
encoding-less databases.
``SQLALCHEMY_POOL_SIZE`` The size of the database pool. Defaults
to the engine's default (usually 5)
``SQLALCHEMY_POOL_TIMEOUT`` Specifies the connection timeout for the
pool. Defaults to 10.
``SQLALCHEMY_POOL_RECYCLE`` Number of seconds after which a
connection is automatically recycled.
This is required for MySQL, which removes
connections after 8 hours idle by
default. Note that Flask-SQLAlchemy
automatically sets this to 2 hours if
MySQL is used.
``SQLALCHEMY_MAX_OVERFLOW`` Controls the number of connections that
can be created after the pool reached
its maximum size. When those additional
connections are returned to the pool,
they are disconnected and discarded.
``SQLALCHEMY_COMMIT_ON_TEARDOWN`` Commit session when the app context is
torn down, unless there was an exception.
``SQLALCHEMY_TRACK_MODIFICATIONS`` If set to `True` (the default)
Flask-SQLAlchemy will track
modifications of objects and emit
signals. This requires extra memory
and can be disabled if not needed.
================================== =========================================

.. versionadded:: 0.8
The ``SQLALCHEMY_NATIVE_UNICODE``, ``SQLALCHEMY_POOL_SIZE``,
Expand All @@ -68,6 +73,9 @@ A list of configuration keys currently understood by the extension:
.. versionadded:: 0.17
The ``SQLALCHEMY_MAX_OVERFLOW`` configuration key was added.

.. versionadded:: 2.0
The ``SQLALCHEMY_TRACK_MODIFICATIONS`` configuration key was added.

Connection URI Format
---------------------

Expand Down
66 changes: 46 additions & 20 deletions flask_sqlalchemy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from threading import Lock
from sqlalchemy import orm, event
from sqlalchemy.orm.exc import UnmappedClassError
from sqlalchemy.orm.session import Session
from sqlalchemy.orm.session import Session as SessionBase
from sqlalchemy.event import listen
from sqlalchemy.engine.url import make_url
from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
Expand Down Expand Up @@ -133,14 +133,30 @@ def _calling_context(app_path):
return '<unknown>'


class _SignallingSession(Session):
class SignallingSession(SessionBase):
"""The signalling session is the default session that Flask-SQLAlchemy
uses. It extends the default session system with bind selection and
modification tracking.
If you want to use a different session you can override the
:meth:`SQLAlchemy.create_session` function.
.. versionadded:: 2.0
"""

def __init__(self, db, autocommit=False, autoflush=False, **options):
#: The application that this session belongs to.
self.app = db.get_app()
self._model_changes = {}
Session.__init__(self, autocommit=autocommit, autoflush=autoflush,
bind=db.engine,
binds=db.get_binds(self.app), **options)
#: A flag that controls weather this session should keep track of
#: model modifications. The default value for this attribute
#: is set from the ``SQLALCHEMY_TRACK_MODIFICATIONS`` config
#: key.
self.emit_modification_signals = \
self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS']
SessionBase.__init__(self, autocommit=autocommit, autoflush=autoflush,
bind=db.engine,
binds=db.get_binds(self.app), **options)

def get_bind(self, mapper, clause=None):
# mapper is None if someone tries to just get a connection
Expand All @@ -150,27 +166,27 @@ def get_bind(self, mapper, clause=None):
if bind_key is not None:
state = get_state(self.app)
return state.db.get_engine(self.app, bind=bind_key)
return Session.get_bind(self, mapper, clause)
return SessionBase.get_bind(self, mapper, clause)


class _SessionSignalEvents(object):

def register(self):
listen(Session, 'before_commit', self.session_signal_before_commit)
listen(Session, 'after_commit', self.session_signal_after_commit)
listen(Session, 'after_rollback', self.session_signal_after_rollback)
listen(SessionBase, 'before_commit', self.session_signal_before_commit)
listen(SessionBase, 'after_commit', self.session_signal_after_commit)
listen(SessionBase, 'after_rollback', self.session_signal_after_rollback)

@staticmethod
def session_signal_before_commit(session):
if not isinstance(session, _SignallingSession):
if not isinstance(session, SignallingSession):
return
d = session._model_changes
if d:
before_models_committed.send(session.app, changes=d.values())

@staticmethod
def session_signal_after_commit(session):
if not isinstance(session, _SignallingSession):
if not isinstance(session, SignallingSession):
return
d = session._model_changes
if d:
Expand All @@ -179,7 +195,7 @@ def session_signal_after_commit(session):

@staticmethod
def session_signal_after_rollback(session):
if not isinstance(session, _SignallingSession):
if not isinstance(session, SignallingSession):
return
session._model_changes.clear()

Expand All @@ -206,7 +222,7 @@ def mapper_signal_after_update(self, mapper, connection, target):
@staticmethod
def _record(mapper, target, operation):
s = orm.object_session(target)
if isinstance(s, _SignallingSession):
if isinstance(s, SignallingSession) and s.emit_modification_signals:
pk = tuple(mapper.primary_key_from_instance(target))
s._model_changes[pk] = (target, operation)

Expand Down Expand Up @@ -236,9 +252,9 @@ def after_cursor_execute(self, conn, cursor, statement,
if queries is None:
queries = []
setattr(ctx, 'sqlalchemy_queries', queries)
queries.append( _DebugQueryTuple( (
queries.append(_DebugQueryTuple((
statement, parameters, context._query_start_time, _timer(),
_calling_context(self.app_package) ) ) )
_calling_context(self.app_package))))


def get_debug_queries():
Expand Down Expand Up @@ -669,13 +685,22 @@ def metadata(self):
return self.Model.metadata

def create_scoped_session(self, options=None):
"""Helper factory method that creates a scoped session."""
"""Helper factory method that creates a scoped session. It
internally calls :meth:`create_session`.
"""
if options is None:
options = {}
scopefunc=options.pop('scopefunc', None)
return orm.scoped_session(
partial(_SignallingSession, self, **options), scopefunc=scopefunc
)
scopefunc = options.pop('scopefunc', None)
return orm.scoped_session(partial(self.create_session, options),
scopefunc=scopefunc)

def create_session(self, options):
"""Creates the session. The default implementation returns a
:class:`SignallingSession`.
.. versionadded:: 2.0
"""
return SignallingSession(self, **options)

def make_declarative_base(self):
"""Creates the declarative base."""
Expand All @@ -700,6 +725,7 @@ def init_app(self, app):
app.config.setdefault('SQLALCHEMY_POOL_RECYCLE', None)
app.config.setdefault('SQLALCHEMY_MAX_OVERFLOW', None)
app.config.setdefault('SQLALCHEMY_COMMIT_ON_TEARDOWN', False)
app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', True)

if not hasattr(app, 'extensions'):
app.extensions = {}
Expand Down

0 comments on commit 575fa65

Please sign in to comment.