跳转到主要内容

使用SQLAlchemy查询数据库的功能方法

项目描述

SQLConstruct 是使用 SQLAlchemy 库查询数据库的功能方法。它被编写来提高速度,同时不引入难以维护和冗长的代码。相反,代码变得更加简单,因此发生错误的机会更少。

它旨在解决的问题

  • 只读 SELECT 查询中的ORM开销;

  • 加载不必要的列时的网络流量;

  • 代码复杂性;

  • N+1问题。

最终

您描述您从数据库中想要获取的内容

from sqlconstruct import Construct, if_

product_struct = Construct({
    'name': Product.name,
    'url': url_for_product.defn(Product),
    'image_url': if_(
        Image.id,
        then_=url_for_image.defn(Image, 100, 100),
        else_=None,
    ),
})

然后您就可以获取它。 SQLConstruct 知道您需要哪些列以及如何将它们转换为可用的格式

>>> product = (
...     session.query(product_struct)
...     .outerjoin(Product.image)
...     .first()
... )
...
>>> product.name
'Foo product'
>>> product.url
'/p1-foo-product.html'
>>> product.image_url
'//images.example.st/123-100x100-foo.jpg'

完整故事

基本准备

from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy.orm import Session, relationship, eagerload
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite://')
Base = declarative_base()

class Image(Base):
    __tablename__ = 'image'

    id = Column(Integer, primary_key=True)
    name = Column(String)

class Product(Base):
    __tablename__ = 'product'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    image_id = Column(Integer, ForeignKey(Image.id))
    description = Column(Text)

    image = relationship(Image)

Base.metadata.create_all(engine)

session = Session(engine)
session.add(Product(name='Foo product', image=Image(name='Foo.jpg')))
session.commit()

def slugify(name):
    # very dumb implementation, just for an example
    return name.lower().replace(' ', '-')

def url_for_product(product):
    return '/p{id}-{name}.html'.format(
        id=product.id,
        name=slugify(product.name),
    )

def url_for_image(image, width, height):
    return '//images.example.st/{id}-{width}x{height}-{name}'.format(
        id=image.id,
        width=width,
        height=height,
        name=slugify(image.name),
    )

常规方法

>>> product = (
...     session.query(Product)
...     .options(eagerload(Product.image))
...     .first()
... )
...
>>> product.name
u'Foo product'
>>> url_for_product(product)
'/p1-foo-product.html'
>>> url_for_image(product.image, 100, 100) if product.image else None
'//images.example.st/1-100x100-foo.jpg'

缺点

  • description 列不是延迟加载的,它将在每次加载时被加载;

  • 如果您将 description 列标记为延迟加载,这可能会在您的项目中其他地方引入N+1问题;

  • 如果您忘记 eagerload Product.image,您也会遇到N+1问题;

  • 您必须将模型实例作为参数在项目的每个地方传递,这往往会导致代码复杂性,因为您不知道它们将来会如何被使用;

  • 模型实例的创建并不便宜,CPU时间随着列数的增加而增加,即使它们都是延迟加载的。

初始解决方案

from sqlconstruct import Construct, apply_, if_

def url_for_product(product_id, product_name):
    return '/p{id}-{name}.html'.format(
        id=product_id,
        name=slugify(product_name),
    )

def url_for_image(image_id, image_name, width, height):
    return '//images.example.st/{id}-{width}x{height}-{name}'.format(
        id=image_id,
        width=width,
        height=height,
        name=slugify(image_name),
    )

product_struct = Construct({
    'name': Product.name,
    'url': apply_(url_for_product, args=[Product.id, Product.name]),
    'image_url': if_(
        Image.id,
        then_=apply_(url_for_image, args=[Image.id, Image.name, 100, 100]),
        else_=None,
    ),
})

用法

>>> product = (
...     session.query(product_struct)
...     .outerjoin(Product.image)
...     .first()
... )
...
>>> product.name
u'Foo product'
>>> product.url
'/p1-foo-product.html'
>>> product.image_url
'//images.example.st/1-100x100-foo.jpg'

优点

  • 只加载所需内容,无额外网络流量,无需延迟/取消延迟列;

  • url_for_producturl_for_image 函数无法增加复杂性,因为它们被迫将所有需要的列作为参数定义;

  • 你正在使用预计算的值(在这个例子中是url)。

缺点

  • 函数代码难以重构和重用,因为每次都应该指定或传递所有参数;

  • 对连接操作要小心,因为如果不显式指定,SQLAlchemy 将产生表的笛卡尔积(SELECT ... FROM product, image WHERE ...),这将返回错误的结果并损害你的性能。

为了解决第一个缺点,SQLConstruct 提供了 define 装饰器,它赋予你定义混合函数的能力,以便以不同的方式使用它们。

from sqlconstruct import define

@define
def url_for_product(product):
    def body(product_id, product_name):
        return '/p{id}-{name}.html'.format(
            id=product_id,
            name=slugify(product_name),
        )
    return body, [product.id, product.name]

@define
def url_for_image(image, width, height):
    def body(image_id, image_name, width, height):
        return '//images.example.st/{id}-{width}x{height}-{name}'.format(
            id=image_id,
            width=width,
            height=height,
            name=slugify(image_name),
        )
    return body, [image.id, image.name, width, height]

现在这些函数可以按以下方式使用

>>> product = session.query(Product).first()
>>> url_for_product(product)  # objective style
'/p1-foo-product.html'
>>> url_for_product.defn(Product)  # apply_ declaration
<sqlconstruct.apply_ at 0x000000000>
>>> url_for_product.func(product.id, product.name)  # functional style
'/p1-foo-product.html'

修改后的最终 Construct 定义

product_struct = Construct({
    'name': Product.name,
    'url': url_for_product.defn(Product),
    'image_url': if_(
        Image.id,
        then_=url_for_image.defn(Image, 100, 100),
        else_=None,
    ),
})

安装

要安装 SQLConstruct,只需

$ pip install sqlconstruct

测试过的 Python 版本:2.7,3.4,3.8。

测试过的 SQLAlchemy 版本:1.0,1.1,1.2,1.3。

上述示例使用 SQLAlchemy >= 0.9,如果你使用的是旧版本,你将不得不在项目配置中做出以下更改。

from sqlconstruct import QueryMixin
from sqlalchemy.orm.query import Query as BaseQuery

class Query(QueryMixin, BaseQuery):
    pass

session = Session(engine, query_cls=Query)

Flask-SQLAlchemy

from flask.ext.sqlalchemy import SQLAlchemy

db = SQLAlchemy(app, session_options={'query_cls': Query})

db = SQLAlchemy(session_options={'query_cls': Query})
db.init_app(app)

许可证

SQLConstruct 采用 BSD 许可证发布。有关更多详细信息,请参阅 LICENSE.txt。

项目详情


下载文件

下载适用于您平台的应用程序。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源分发

SQLConstruct-0.2.4.tar.gz (9.9 kB 查看哈希)

上传时间

由以下赞助

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面