Flask-SQLAlchemy源碼學(xué)習(xí)

博客版本: 0.1 Initial Release.

Prerequisites(Very Important!!!):

  • 熟悉SQLAlchemy常用用法, 一個(gè)很好的教程來(lái)自于SQLAlchmey作者M(jìn)ike Bayer在PyCon13的Tutorial.
  • 熟悉Flask官方文檔的關(guān)于如何寫一個(gè)SQLite3的Flask Extension的示例.
  • 熟悉Python的一些高級(jí)特性:描述符,metaclass.
  • initnew的區(qū)別.
  • 對(duì)數(shù)據(jù)庫(kù)有一定了解颂碧,推薦Stanford CS145的數(shù)據(jù)庫(kù)入門課程.

Goals:

  • 學(xué)習(xí)Flask的插件是怎么工作的.
  • 增加對(duì)SQLAlchemy的認(rèn)識(shí).
  • 學(xué)習(xí)Flask中teardown_appcontext的用法.

Table of Contents


Part1 定義一張表


Part2 創(chuàng)建一張表


Part3 查詢一張表


Part4 miscellanies


Part1 定義一張表


天才第一步南吮,讓我們先從定義一個(gè)表開始吧.
在SQLAlchemy中,以O(shè)RM方式定義表有兩種方法,分別是Classical vs. Declarative.
Flask-SQLAlchemy主要使用的是Declarative方法.
首先, 我們回顧一下在原生SQLAlchemy中是如何進(jìn)行Declarative定義的。

# 1.py
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

from sqlalchemy import Column, Integer, String

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    email = Column(String(30))

    def __repr__(self):
        return "<User(%r, %r)>" % self.username

Declarative方法本質(zhì)上就是自動(dòng)完成了Classical方法所做的事情.所謂Declarative方式的意思就是說(shuō)什么就有什么的方式,
比如,要有光,就有光,定義完一個(gè)User類次员,要有Table表,于是也就在Metadata中有了Table表.
當(dāng)User定義完成時(shí)王带,User class現(xiàn)在就有了一個(gè)Table object.可以通過(guò)User.table或者
User.metadata.tables['user']來(lái)訪問(wèn).The Declarative extension uses a Python metaclass, which is a handy way to run a series of operations each time a new class is first declared, to generate a new Table object from what's been declared, and pass it to the mapper funcion along with the class.
在Flask-SQLAlchemy中淑蔚,Model是對(duì)SQLAlchemy的Base的包裝,
在Flask-SQLAlchemy中,我們將Base改為db.Model.

# 2.py
class User(db.Model):

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

    def __init__(self, username, email):
        self.username = username
        self.email = email

    def __repr__(self):
        return '<User %r>' % self.username

下面我們來(lái)看看db.Model:

# 3.py
self.Model = self.make_declarative_base(metadata)
...
def make_declarative_base(self, metadata=None):
        """Creates the declarative base."""
        base = declarative_base(cls=Model, name='Model',
                                metadata=metadata,
                                metaclass=_BoundDeclarativeMeta)
        base.query = _QueryProperty(self)
        return base

在SQLAlchemy的官方文檔中查詢declarative_base的API.

# SQLAlchemy declarative_base() API.
def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
                     name='Base', constructor=_declarative_constructor,
                     class_registry=None,
                     metaclass=DeclarativeMeta):

可知各個(gè)參數(shù)的意義:
*1. bind - An optional Connectable, will be assigned the bind attribute on the MetaData instance.
這里的Connectable這個(gè)接口的實(shí)現(xiàn)有:Engine和Connection.
*2. metadata - All Table objects implicitly declared by subclasses of the base will share this
MetaData.
*3. mapper - Will be used to map subclasses to their Tables.
*4. cls - A type to use as the base for the generated declarative base class.
*5. name - The display name for the generated class.
*6. constructor - ?????
*7. class_registry - ?????
*8. metaclass - A metaclass or metaclass compatible callable to use as the meta type of the
meta type of the generated declarative base class.

首先metaclass(創(chuàng)造class的class)默認(rèn)設(shè)置為DeclarativeMeta辫秧,這是最重要的設(shè)置束倍,
DeclarativeMeta具體我們不會(huì)去剖析被丧,但是根據(jù)metaclass的用途盟戏,我們可以大膽猜測(cè),
定義類User時(shí)甥桂,在metaclass創(chuàng)造User這個(gè)class時(shí)柿究,會(huì)做很多背后的設(shè)置工作,具體做些什么呢:

The Declarative extension uses a Python metaclass, which is a
handy way to run a series of operations each time a new class is first declared, to generate a 
new Table object from what's been declared, and pass it to the mapper funcion along with the 
class.

在3.py中黄选,我們?cè)O(shè)置declarative_base的參數(shù)有:

  • cls = Model
  • metadata = metadata(None)
  • metaclass = _BoundDeclarativeMeta

_BoundDeclarativeMeta是對(duì)DeclarativeMeta的一個(gè)簡(jiǎn)單包裝:

# 4.py
class _BoundDeclarativeMeta(DeclarativeMeta):

    def __new__(cls, name, bases, d):
        if _should_set_tablename(bases, d):
            def _join(match):
                word = match.group()
                if len(word) > 1:
                    return ('_%s_%s' % (word[:-1], word[-1])).lower()
                return '_' + word.lower()
            d['__tablename__'] = _camelcase_re.sub(_join, name).lstrip('_')

        return DeclarativeMeta.__new__(cls, name, bases, d)

    def __init__(self, name, bases, d):
        bind_key = d.pop('__bind_key__', None)
        DeclarativeMeta.__init__(self, name, bases, d)
        if bind_key is not None:
            self.__table__.info['bind_key'] = bind_key

_BoundDeclarativeMeta實(shí)現(xiàn)的功能就是自動(dòng)添加tablename屬性的設(shè)置, 在SQLAlchemy中蝇摸,
比如在1.py中,tablename的設(shè)置是必須的办陷,但是在2.py這項(xiàng)并不是必須貌夕,原因就是Flask-SQLAlchemy
在背后幫你自動(dòng)設(shè)置好了.
首先我們思考一個(gè)問(wèn)題在2.py中,在定義(創(chuàng)造民镜?)User這個(gè)class過(guò)程中啡专,
傳入new中的參數(shù)會(huì)是哪些呢?設(shè)置打印語(yǔ)句可知:
cls ---> <class 'flask_sqlalchemy._BoundDeclarativeMeta'>
name ---> User
bases ---> (<class 'flask_sqlalchemy.Model'>,)
d ---> {'module': ..., 'init': ..., 'id':..., 'qualname': ...,
'username': ..., 'email': ..., 'repr':...}
下面我們看 _should_set_tablename干了啥:

def _should_set_tablename(bases, d):

    # 如果d中制圈,也就是User.__dict__中含有這些屬性们童,則不用自動(dòng)設(shè)置.
    if '__tablename__' in d or '__table__' in d or '__abstract__' in d:
        return False

    # 檢查User中的Column屬性,如果任何一個(gè)Column屬性為primary_key,則返回True.
    if any(v.primary_key for v in itervalues(d) if isinstance(v, sqlalchemy.Column)):
        return True

    # Joined table inheritance without explicitly naming sub-models.?????
    for base in bases:
        if hasattr(base, '__tablename__') or hasattr(base, '__table__'):
            return False

        for name in dir(base):
            attr = getattr(base, name)

            if isinstance(attr, sqlalchemy.Column) and attr.primary_key:
                return True

如果 _should_set_tablename返回True鲸鹦,我們就會(huì)設(shè)置d['tablename'],設(shè)置的具體格式就忽略.
設(shè)置完后慧库,我們?cè)僬{(diào)用SQLAlchemy Base默認(rèn)的metaclass的new方法.

_BoundDeclarativeMeta在init中還對(duì)bind_key進(jìn)行了設(shè)置,這個(gè)
跟 MULTIPLE DATABASES WITH BINDS有關(guān)馋嗜,本小節(jié)先忽略.

Part2 - 創(chuàng)建一張表


在SQLAlchemy中齐板,我們可以這樣創(chuàng)建一個(gè)數(shù)據(jù)庫(kù):

from sqlalchemy import create_engine
engine = create_engine('sqlite:///SALTRIVER.db', echo=True)
Base.metadata.create_all(engine)

于是,在電腦硬盤上就生成了一個(gè)SALTRIVER.db的數(shù)據(jù)庫(kù)文件.
那么在Flask-SQLAlchemy中是如何對(duì)其進(jìn)行包裝的呢?
下面我們看看當(dāng)定義完User后甘磨,執(zhí)行db.create_all()會(huì)發(fā)生什么.
以下幾段代碼重點(diǎn)在于我們是怎么獲取engine的听皿,也就是怎么與一個(gè)engine進(jìn)行綁定(bind).

在分析以下幾段代碼的時(shí)候,請(qǐng)先熟讀Flask-SQLAlchemy官方文檔中的第六章.
為熟悉用法,示例代碼可參考test_sqlalchemy.py中的BindsTestCase
將結(jié)合示例代碼對(duì)源碼進(jìn)行說(shuō)明

關(guān)于多引擎綁定宽档,還有一種django-style.詳見(jiàn)Mike Bayer的博客.

在此之前尉姨,我們應(yīng)該知道在init_app中我們?cè)O(shè)置了SQLALCHEMY_DATABASE_URI和
SQLALCHEMY_BINDS兩項(xiàng)參數(shù).

    # bind參數(shù)默認(rèn)為'__all__'
    def create_all(self, bind='__all__', app=None):
       
        self._execute_for_all_tables(app, bind, 'create_all')

調(diào)用db.create_all,db.drop_all,db.reflect都會(huì)調(diào)用 _execute_for_all_tables().

    def _execute_for_all_tables(self, app, bind, operation, skip_tables=False):
        
        # 首先獲取app.
        app = self.get_app(app)

        if bind == '__all__':
            # '__all__'的意思就是把每個(gè)表創(chuàng)建到對(duì)應(yīng)的引擎的中.
            # 設(shè)置過(guò)__bind_key__屬性的,創(chuàng)建到bind對(duì)應(yīng)的引擎.
            # 沒(méi)設(shè)置過(guò)__bind_key__吗冤,就創(chuàng)建到默認(rèn)的引擎中---'SQLALCHEMY_DATABASE_URI'.
            # binds列表中的None就對(duì)應(yīng)沒(méi)bind過(guò)引擎的表.
            # 對(duì)于示例代碼就是:Foo -> foo, Bar -> bar, Baz -> default.
            binds = [None] + list(app.config.get('SQLALCHEMY_BINDS') or ())
        elif isinstance(bind, string_types) or bind is None:
            # 如果明確指定, binds就設(shè)置為只含有bind的列表.
            # 意思是只在該bind對(duì)應(yīng)的engine中創(chuàng)建Tables.
            binds = [bind]
        else:
            # 比如binds為一個(gè)自行指定的列表.
            binds = bind
        
        # 注意skip_tables=False
        for bind in binds:
            extra = {}
            if not skip_tables:
                # 獲取一個(gè)bind相關(guān)的tables. 
                tables = self.get_tables_for_bind(bind)
                extra['tables'] = tables
            # 獲得self.Model.metadata.create_all方法.
            op = getattr(self.Model.metadata, operation)
            # 執(zhí)行self.Model.metadata.create_all(bind=engine)
            # 原生的create_all可以帶個(gè)可選參數(shù)tables, which is a 
            # subset of the total tables in the metadata.
            # 只創(chuàng)建指定的Tables.
            op(bind=self.get_engine(app, bind), **extra)
    def get_tables_for_bind(self, bind=None):
        """Returns a list of all tables relevant for a bind."""
        result = []
        for table in itervalues(self.Model.metadata.tables):
            # bind為None,代表對(duì)應(yīng)的默認(rèn)引擎.
            if table.info.get('bind_key') == bind:
                result.append(table)
        return result
下面我們看看是如何獲得get_engine的.
       # SQLAlchemy.get_engine()
       def get_engine(self, app, bind=None):
           # 獲得一把鎖. 思考:為什么需要上鎖又厉?
           with self._engine_lock:
               # 獲得_SQLAlchemyState實(shí)例.
               # state = app.extensions['sqlalchemy'].
               state = get_state(app)
               # 初次獲取engine的時(shí)候,connector為None.
               connector = state.connectors.get(bind)
               if connector is None:
                   # 創(chuàng)建一個(gè)_EngineConnector實(shí)例.
                   connector = self.make_connector(app, bind)
                   # 將_EngineConnector加入(緩存)到state.connectors中.
                   state.connectors[bind] = connector
               # 再調(diào)用_EngineConnector的get_engine().
               return connector.get_engine()
    _EngineConnector主要作用是用來(lái)緩存engine的.
           # _EngineConnector.get_engine()
           def get_engine(self):
               # 思考: 為什么要加瑣?
               with self._lock:
                   # uri = "sqlite:///XYZ.db"
                   uri = self.get_uri()
                   echo = self._app.config['SQLALCHEMY_ECHO']
                   # 最開始, _connected_for為None.
                   # _connected_for為engine唯一identity(見(jiàn)下方).
                   if (uri, echo) == self._connected_for:
                       # 第二次訪問(wèn)該engine時(shí)椎瘟,返回之前緩存在
                       # _EngineConnector._engine中的engine.
                       return self._engine
                   info = make_url(uri)
                   options = {'convert_unicode': True}
                   # 設(shè)置SQLALCHEMY相關(guān)的參數(shù), 包括:
                   # 1, SQLALCHEMY_POOL_SIZE
                   # 2, SQLALCHEMY_POOL_RECYCLE
                   # 3, SQLALCHEMY_POOL_TIMEOUT
                   # 4, SQLALCHEMY_MAX_OVERFLOW
                   self._sa.apply_pool_defaults(self._app, options)
                   # What the fuck of this????????????????????????????
                   self._sa.apply_driver_hacks(self._app, info, options)
                   if echo:
                       options['echo'] = True
                   # 終于, 創(chuàng)建了一個(gè)engine.
                   self._engine = rv = sqlalchemy.create_engine(info, **options)
                   # - - - - - - - - - - - - - - - - - - - - - - - - - 
                   # 暫時(shí)忽略.
                   if _record_queries(self._app):
                       _EngineDebuggingSignalEvents(self._engine,
                                                    self._app.import_name).register()
                   # - - - - - - - - - - - - - - - - - - - - - - - - - 
                   # 設(shè)置self._connected_for.
                   # _connected_for相當(dāng)于engine的一個(gè)唯一identity.
                   # 通過(guò)比較(uri, echo) == self._connected_for是否相等來(lái)
                   # 判斷某個(gè)engine是非緩存在_EngineConnector中.
                   self._connected_for = (uri, echo)
                   # 返回engine.
                   return rv

返回engine后覆致,最后調(diào)用self.Model.metadata.create_all(bind=engine, **extra).
成功創(chuàng)建數(shù)據(jù)庫(kù)文件.

一個(gè)助于理解的示意圖為:
[PICTURE HERE]
感覺(jué)有循環(huán)引用問(wèn)題?

Part3 查詢一張表


Part3.1 scoped_session

以SQLAlchemy的ORM方式肺蔚,我們通常是這樣開始查詢的:

from sqlalchemy.orm import Session
session = Session(engine)
query = session.query(User)

在Flask-SQLAlchemy中煌妈,我們是如何對(duì)其進(jìn)行包裝改造的呢?
在SQLAlchemy中的init函數(shù)中設(shè)置了scopefunc.
這個(gè)scopefunc與Thread Local Data有關(guān)宣羊,用來(lái)識(shí)別Thread Local Data.

...
if session_options is None:
            session_options = {}

session_options.setdefault('scopefunc', connection_stack.__ident_func__)
...
self.session = self.create_scoped_session(session_options)
...
        def create_scoped_session(self, options=None):
          
            if options is None:
                options = {}
            scopefunc = options.pop('scopefunc', None)
            
            return orm.scoped_session(partial(self.create_session, options),
                                      scopedfunc=scopefunc)

Very Important!!!

繼續(xù)之前請(qǐng)仔細(xì)閱讀SQLAlchemy Documentation Release 1.1.0b1中
2.5.6 Contextual/Thread-local Sessions小節(jié)(p233-p238),
一定要仔細(xì)閱讀.在此我就不翻譯了.

下面看看self.create_session: SignallingSession繼承Session璧诵,對(duì)get_bind方法進(jìn)行了重寫.
SignallingSession是Flask-SQLAlchemy默認(rèn)的session.如果想使用一個(gè)不一樣的session.
可對(duì)self.create_session進(jìn)行重寫.

def create_session(self, options):
    return SignallingSession(self, **options)
    # SignallingSession.__init__().
    def __init__(self, db, autocommit=False, autoflush=True, app=None, **options):
    
        self.app = app = db.get_app()
        # 暫時(shí)忽略.見(jiàn)event小節(jié).
        track_modifications = app.config['SQLALCHEMY_TRACK_MODIFICATIONS']

        # 獲得bind參數(shù).
        # 單獨(dú)的一個(gè)session需要與一個(gè)engine或者connection綁定,
        # 所以SQL操作通過(guò)這個(gè)Connectable完成.
        # 如果沒(méi)有明確指定bind參數(shù),bind設(shè)置為db.engine.
        # db.engine為默認(rèn)的engine(SQLALCHEMY_DATABASE_URI).
        bind = options.pop('bind', None) or db.engine

        # 獲得binds參數(shù), 注意bind與binds的區(qū)別.
        # db.get_binds() Returns a dictionary with a table->engine mapping.
        # binds中的映射還可以是: somemapper --> engine. SomeMappedClass --> engine.
        # 對(duì)于示例代碼而言,類似于:
        # { Table_baz: Engine(sqlite://),
        #   Table_bar: Engine(sqlite:///XXX),
        #   Table_foo: Engine(sqlite:///YYY)
        # }
        binds = options.pop('binds', None) or db.get_binds(app)

        # 暫時(shí)忽略.見(jiàn)event小節(jié).
        if track_modifications is None or track_modifications:
            _SessionSignalEvents.register(self)

        # 調(diào)用原生session的__init__<官方文檔p246>.
        SessionBase.__init__(
            self, autocommit=autocommit, autoflush=autoflush,
            bind=bind, binds=binds, **options
        )

說(shuō)說(shuō)SessionBase.init中的參數(shù),注意比較bind和binds的區(qū)別.
bind: An optional Engine or Connectable to which this Session should bound.
When specified, all SQL operations performed by this session will execute
via this Connectable.
binds: binds與multiply-bound有關(guān).假如像這樣傳入binds:

Session = sessionmaker(binds={
    SomeMappedClass: create_engine('postgresql://engine1'),
    somemapper: create_engine('postgresql://engine2'),

    # Flask-SQLAlchemy主要使用這種類型的映射.
    some_table: create_engine('postgresql://engine3'),
    })

那么,session最后是如何決定是跟哪個(gè)engine進(jìn)行鏈接的呢仇冯?
下面來(lái)看看重寫的get_bind()方法,因?yàn)間et_bind()會(huì)決定到底用binds中的哪一個(gè)bind參數(shù):
|
|
V
```
def get_bind(self, mapper=None, clause=None):

    # 關(guān)于mapper, clause會(huì)傳入什么.見(jiàn)'EXTRA MATERIAL'
    if mapper is not None:

        # 比如session.query(User).all()中, 是怎么獲取bind的呢之宿?
        # 知道User class, 便知道了User class的mapper, User.__mapper__,
        # 知道了mapper, 便知道了mapper.mapped_table.
        # 在Runtime時(shí), 傳入mapper的參數(shù)就是User.__mapper__.
        # 在_BoundDeclarativeMeta.__init__函數(shù)中設(shè)置了
        # mapped_table的info屬性.
        info = getattr(mapper.mapped_table, 'info', {})

        # 獲取bind_key參數(shù).
        bind_key = info.get('bind_key')
        if bind_key is not None:
            state = get_state(self.app)
            return state.db.get_engine(self.app, bind=bind_key)

    # 如果bind_key為None,也就是表沒(méi)有與任何engine綁定,調(diào)用原生的get_bind().
    # 原生是怎么決定engine的呢?這需要看sqlalchemy的源碼了.但是這里我們可以
    # 猜想一下大概會(huì)是這樣決定engine的: Baz->mapper->mapped_table
    # ->metadata->binded_engine. 因?yàn)樵谧畛鮿?chuàng)建Baz這張表的時(shí)候苛坚,會(huì)與Baz
    # 這張表所在的metadata在調(diào)用metadata.create_all(bind=engine)中的engine
    # 進(jìn)行綁定.到底是不是這樣呢?有興趣可以查看sqlalchemy中的源碼.
    return SessionBase.get_bind(self, mapper, clause)
```

此處應(yīng)該有一個(gè)示意圖.
==========================EXTRA MATERIAL=============================
在仔細(xì)剖析重寫的get_bind()函數(shù)之前比被,先來(lái)看看文檔是怎么說(shuō)原生的get_bind()函數(shù)的:
```
get_bind(mapper=None, clause=None)
----------------------------------
For a multiply-bound or unbound Session, the mapper or clause arguments are
used to determine the appropriate bind to return.

Note that the "mapper" argument is usually present when Session.get_bind() is 
called via an ORM operation such as a Session.query(), each individual 
INSERT/UPDATE/DELETE operation within a Session.flush(), call, etc.

The order of resolution is:
    ...

Parameters:
    * mapper - Optional mapper() mapped class or instance of Mapper. The bind 
    can be derived from a Mapper first by consulting the "binds" map associated
    with this Session, and secondly by consultiong the MetaData associated with
    the Table to which the Mapper is mapped for a bind.

    * clause - A ClauseElement (i.e. select(), text(), etc.). If the mapper argument
    is not present or could not produce a bind, the given expression construct will
    be searched for a bound element, typically a Table associated with bound MetaData.
```

下面我們來(lái)看一些示例代碼來(lái)看看get_bind函數(shù)中傳入的參數(shù)到底是什么.
示例代碼選自Mike Bayer的博客, 完整代碼見(jiàn)博客.

調(diào)用add_all或者add的時(shí)候,以s.add(Model1(data='river'))為例, 此時(shí)傳入的參數(shù)為:
Mapper為將Model1映射為model1的Mapper, Clause為None.
with s.begin(): # 11. Writes go to "leader".... s.add_all([ Model1(data='m1_a'), Model2(data='m2_a'), Model1(data='m1_b'), Model2(data='m2_b'), Model3(data='m3_a'), Model3(data='m3_b'), ])

Mapper為將Model1映射為model1的Mapper,Clause為:
<class 'sqlalchemy.sql.expression.Select'>
s.query(Model1).all()

Mapper和Clause都為None.
```
# - - - - - - Added by myself - - - - - - -
from sqlalchemy import select

model1 = Model1.__mapper__.tables[0]

sel = select([model1.c.data])
d = s.connection().execute(sel).fetchall() 
# - - - - - - - - - - - - - - - - - - - - -
```

===================================================================

- - - - - - - - - - - - - - - - - - - - - - - -

db.session.add(user1)
db.session.commit()

當(dāng)一個(gè)請(qǐng)求結(jié)束后泼舱,如何處理session呢等缀?

        ...
        # 0.9 and later
        if hasattr(app, 'teardown_appcontext'):
            teardown = app.teardown_appcontext
        ...

        @teardown
        def shutdown_session(response_or_exc):
            if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
                if response_or_exc is None:
                    self.session.commit()
            self.session.remove()
            return response_or_exc

在SQLAlchemy.init_app()中,我們?yōu)閍pp設(shè)置teardown_appcontext回調(diào)函數(shù).
在Flask中娇昙,每當(dāng)一個(gè)request結(jié)束后尺迂,都會(huì)執(zhí)行注冊(cè)過(guò)@teardown_appcontext的函數(shù),
可以看到在shutdown_session函數(shù)中涯贞,會(huì)調(diào)用self.session.remove()辜纲,將session管理的資源缩麸,
比如connection,返回到connection pool.

Part3.2 - Custom Query Object.

在Flask-SQLAlchemy中,我們還可以這樣查詢:

User.query.first()

或者通沉泻穑可以這樣:

db.session.query(User).first()

第二種很熟悉了, 我們看看第一種是怎么實(shí)現(xiàn)的.

class Model(object):

    query_class = BaseQuery

    query = None

|
V

def make_declarative_base(self, metadata=None):
        # 所有繼承db.Model(也就是base)的類(如User)的基類都將被設(shè)置為cls參數(shù),也就是Model.
        # 那么, User也就有了query_class和query屬性.
        base = declarative_base(cls=Model,    # <--- 
                                name='Model',
                                metadata=metadata,
                                metaclass=_BoundDeclarativeMeta)
        base.query = _QueryProperty(self)
        return base

|
V

class _QueryProperty(object):

    def __init__(self, sa):
        self.sa = sa

    def __get__(self, obj, type):
        try:
            mapper = orm.class_mapper(type)
            if mapper:
                return type.query_class(mapper, session=self.sa.session())
        except UnmappedClassError:
            return None

_QueryProperty是個(gè)Non-Data描述符.當(dāng)我們調(diào)用User.query.first()時(shí)或衡,
會(huì)觸發(fā)db.Model.query描述符的get方法執(zhí)行较沪,傳入的參數(shù)obj=None, type=<class User>.
然后獲取User.mapper, orm.class_mapper(type)等同于type.mapper.
最后返回type.query_class(), 也就是type.BaseQuery(),即User.BaseQuery().
BaseQuery繼承自orm.Query.

# Query Object API
class sqlalchemy.orm.query.Query(entities, session=None)

Query的init函數(shù)接受2個(gè)參數(shù)entities和session.實(shí)際上:

# 用法1
session.query(User).all()

# 用法2
Query(User, session).all()

用法1和用法2是一樣的.
于是,type.query_class(mapper, session=self.sa.session())會(huì)返回一個(gè)與
某個(gè)mapper和session一起綁定了的Query Object.
實(shí)際上, User.query.all() 轉(zhuǎn)化為了 session.query(User).all().

Part 4 Miscellanies


1逻翁,BaseQuery中的paginate方法.
paginate主要用來(lái)實(shí)現(xiàn)分頁(yè)功能.一個(gè)簡(jiǎn)單的示例可參考flask web development的11章.
我們一起看看是如何實(shí)現(xiàn)分頁(yè)功能的.

    def paginate(self, page=None, per_page=None, error_out=True):
        ...
        items = self.limit(per_page).offset((page - 1) * per_page).all()

        ...
        else:
            total = self.order_by(None).count()

        return Pagination(self, page, per_page, total, items)

paginate的參數(shù)page代表第幾頁(yè). 這個(gè)參數(shù)是用瀏覽器中的url中的參數(shù)傳遞的.per_page代表沒(méi)頁(yè)有多少個(gè)items. paginate主要干了倆件事薄嫡,第一,獲取該頁(yè)的items.

items = self.limit(per_page).offset((page - 1) * per_page).all()

第二颗胡,獲取total參數(shù).(萬(wàn)一total參數(shù)為百萬(wàn)級(jí)別呢毫深?比如google的搜索結(jié)果?)
然后返回一個(gè)Pagination對(duì)象.我們結(jié)合示例中的模版來(lái)看看Pagination對(duì)象:

{% macro pagination_widget(pagination, endpoint) %}
<ul class="pagination">
    
    # pagination.has_prev().
    <li{% if not pagination.has_prev %} class="disabled"{% endif %}>
        # 如果pagination.has_prev毒姨,該按鈕會(huì)指向前一頁(yè)的url.
        <a href="{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs) }}{% else %}#{% endif %}">
            ?
        </a>
    </li>

    # pagination.iter_pages().
    {% for p in pagination.iter_pages() %}
        {% if p %}
            {% if p == pagination.page %}
            <li class="active">
                <a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
            </li>
            {% else %}
            <li>
                <a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
            </li>
            {% endif %}
        {% else %}
        <li class="disabled"><a href="#">…</a></li>
        {% endif %}
    {% endfor %}

    # pagination.has_next().
    <li{% if not pagination.has_next %} class="disabled"{% endif %}>
        <a href="{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{% else %}#{% endif %}">
            ?
        </a>
    </li>
</ul>
{% endmacro %}
圖片發(fā)自簡(jiǎn)書App

一圖勝千言.

References and Useful Resources:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哑蔫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子弧呐,更是在濱河造成了極大的恐慌闸迷,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俘枫,死亡現(xiàn)場(chǎng)離奇詭異腥沽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鸠蚪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門今阳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人茅信,你說(shuō)我怎么就攤上這事盾舌。” “怎么了汹押?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵矿筝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我棚贾,道長(zhǎng),這世上最難降的妖魔是什么榆综? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任妙痹,我火速辦了婚禮,結(jié)果婚禮上鼻疮,老公的妹妹穿的比我還像新娘怯伊。我一直安慰自己,他們只是感情好判沟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布耿芹。 她就那樣靜靜地躺著,像睡著了一般挪哄。 火紅的嫁衣襯著肌膚如雪吧秕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天迹炼,我揣著相機(jī)與錄音砸彬,去河邊找鬼颠毙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛砂碉,可吹牛的內(nèi)容都是我干的蛀蜜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼增蹭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼滴某!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起滋迈,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤壮池,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后杀怠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體椰憋,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年赔退,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了橙依。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硕旗,死狀恐怖窗骑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漆枚,我是刑警寧澤创译,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站墙基,受9級(jí)特大地震影響软族,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜残制,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一立砸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧初茶,春花似錦颗祝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至折汞,卻和暖如春倔幼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背字支。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工凤藏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奸忽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓揖庄,卻偏偏與公主長(zhǎng)得像栗菜,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蹄梢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容