SQLAlchemy - 使用DateTime列查询按月/日/年进行筛选

Lee*_*der 7 python sqlalchemy flask flask-sqlalchemy

我正在建立一个Flask网站,其中涉及跟踪付款,我遇到了一个问题,我似乎无法按日期过滤我的一个数据库模型.

例如,如果这是我的表的样子:

payment_to, amount, due_date (a DateTime object)

company A, 3000, 7-20-2018
comapny B, 3000, 7-21-2018
company C, 3000, 8-20-2018
Run Code Online (Sandbox Code Playgroud)

我想过滤它,以便我获得7月20日之后的所有行,或者8月份的所有行等.

我可以想到一种原始的,蛮力的方式来过滤所有付款,然后迭代遍历列表按月/年过滤,但我宁愿远离这些方法.

这是我的支付数据库模型:

class Payment(db.Model, UserMixin):
    id = db.Column(db.Integer, unique = True, primary_key = True)

    payment_to = db.Column(db.String, nullable = False)
    amount = db.Column(db.Float, nullable = False)

    due_date = db.Column(db.DateTime, nullable = False, default = datetime.strftime(datetime.today(), "%b %d %Y"))
    week_of = db.Column(db.String, nullable = False)
Run Code Online (Sandbox Code Playgroud)

这是我试图Payment按日期过滤:

Payment.query.filter(Payment.due_date.month == today.month, Payment.due_date.year == today.year, Payment.due_date.day >= today.day).all()
Run Code Online (Sandbox Code Playgroud)

在哪里today简单datetime.today().

我假设due_date当我调用它时,列将具有所有DateTime属性(例如.month),但似乎我错了.

Payment按日期过滤列的最佳方法是什么?谢谢您的帮助.

Chr*_*ski 16

SQLAlchemy有效地将您在Python中表达的查询转换为SQL.但它是在相对肤浅的层面上进行的,基于您Column在定义模型时分配的数据类型.

这意味着它不一定会datetime.datetime在其DateTime构造上复制Python的API - 毕竟,这两个类意味着做非常不同的事情!(datetime.datetime为Python提供日期时间功能,而SQLAlchemy DateTime告诉它的SQL转换逻辑它正在处理SQL DATETIME或TIMESTAMP列).

但别担心!你可以通过不同的方式来实现你想要做的事情,其中​​一些方法非常简单.我认为最简单的三个是:

  1. 使用完整datetime实例构建过滤器,而不是其组件(日,月,年).
  2. extract在过滤器中使用SQLAlchemy的构造.
  3. 在模型中定义三个混合属性,用于返回可以过滤的付款月,日和年.

过滤datetime对象

这是实现您正在尝试的三种(简单)方法中最简单的方法,它也应该以最快的速度执行.基本上,不要试图在查询中单独过滤每个组件(日,月,年),只需使用单个datetime值.

基本上,以下内容应该等同于您在上面的查询中尝试执行的操作:

from datetime import datetime

todays_datetime = datetime(datetime.today().year, datetime.today().month, datetime.today().day)

payments = Payment.query.filter(Payment.due_date >= todays_datetime).all()
Run Code Online (Sandbox Code Playgroud)

现在,payments应该是在系统当前日期的开始(时间00:00:00)之后到期的所有付款.

如果您想变得更复杂,例如过去30天内过滤付款.您可以使用以下代码执行此操作:

from datetime import datetime, timedelta

filter_after = datetime.today() - timedelta(days = 30)

payments = Payment.query.filter(Payment.due_date >= filter_after).all()
Run Code Online (Sandbox Code Playgroud)

您可以使用and_和组合多个过滤器目标or_.例如,要返回在过去30天内应付款项多是因为在15年前,你可以使用:

from datetime import datetime, timedelta
from sqlalchemy import and_

thirty_days_ago = datetime.today() - timedelta(days = 30)
fifteen_days_ago = datetime.today() - timedelta(days = 15)

# Using and_ IMPLICITLY:
payments = Payment.query.filter(Payment.due_date >= thirty_days_ago,
                                Payment.due_date <= fifteen_days_ago).all()

# Using and_ explicitly:
payments = Payment.query.filter(and_(Payment.due_date >= thirty_days_ago,
                                     Payment.due_date <= fifteen_days_ago)).all()
Run Code Online (Sandbox Code Playgroud)

从您的角度来看,这里的技巧是datetime在执行查询之前正确构建过滤器目标实例.

使用extract构造

SQLAlchemy的extract表达式(此处记录)用于执行SQL EXTRACT语句,这就是在SQL中如何从DATETIME/TIMESTAMP值中提取月,日或年.

使用这种方法,SQLAlchemy告诉您的SQL数据库"首先,从我的DATETIME列中提取月,日和年,然后过滤该提取的值".注意,这种方法比上一个过滤较慢datetime如上所述值.但这是如何工作的:

from sqlalchemy import extract

payments = Payment.query.filter(extract('month', Payment.due_date) >= datetime.today().month,
                                extract('year', Payment.due_date) >= datetime.today().year,
                                extract('day', Payment.due_date) >= datetime.today().day).all()
Run Code Online (Sandbox Code Playgroud)

使用混合属性

SQLAlchemy Hybrid属性是很棒的东西.它们允许您透明地应用Python功能而无需修改数据库.我怀疑这个特定的用例可能有点过分,但它们是实现你想要的第三种方式.

基本上,您可以将混合属性视为数据库中实际不存在的"虚拟列",但SQLAlchemy可以在需要时从数据库列中即时计算.

在您的具体问题,我们定义了三种混合属性:due_date_day,due_date_month,due_date_year在你的Payment模型.这是如何工作的:

... your existing import statements

from sqlalchemy import extract
from sqlalchemy.ext.hybrid import hybrid_property

class Payment(db.Model, UserMixin):
    id = db.Column(db.Integer, unique = True, primary_key = True)

    payment_to = db.Column(db.String, nullable = False)
    amount = db.Column(db.Float, nullable = False)

    due_date = db.Column(db.DateTime, nullable = False, default = datetime.strftime(datetime.today(), "%b %d %Y"))
    week_of = db.Column(db.String, nullable = False)

    @hybrid_property
    def due_date_year(self):
        return self.due_date.year

    @due_date_year.expression
    def due_date_year(cls):
        return extract('year', cls.due_date)

    @hybrid_property
    def due_date_month(self):
        return self.due_date.month

    @due_date_month.expression
    def due_date_month(cls):
        return extract('month', cls.due_date)

    @hybrid_property
    def due_date_day(self):
        return self.due_date.day

    @due_date_day.expression
    def due_date_day(cls):
        return extract('day', cls.due_date)

payments = Payment.query.filter(Payment.due_date_year >= datetime.today().year,
                                Payment.due_date_month >= datetime.today().month,
                                Payment.due_date_day >= datetime.today().day).all()
Run Code Online (Sandbox Code Playgroud)

以下是上面的内容:

  1. 你正在定义你的Payment模型.
  2. 但你要添加一些所谓的只读实例的属性due_date_year,due_date_monthdue_date_day.使用due_date_year作为一个例子,这是在操作的实例属性实例您的Payment类.这意味着当您执行one_of_my_payments.due_date_year该属性时将从due_datePython实例中提取值.因为这一切都发生在Python中(即不接触您的数据库),它将对datetime.datetimeSQLAlchemy已存储在您的实例中的已翻译对象进行操作.它将返回结果due_date.year.
  3. 然后你要添加一个属性.这是装饰的一点@due_date_year.expression.这个装饰器告诉SQLAlchemy,当它将引用转换due_date_year为SQL表达式时,它应该按照此方法中的定义进行转换.所以上面的例子告诉SQLAlchemy"如果你需要due_date_year在SQL表达式中使用,那么应该extract('year', Payment.due_date)如何due_date_year表达.

(注意:上面的例子假设due_date_year,due_date_monthdue_date_day,都是只读属性.你当然可以定义自定义setter,也可以使用@due_date_year.setter接受参数(self, value))

结论

在这三种方法中,我认为第一种方法(过滤datetime)既易于理解,也最容易实现,并且执行速度最快.这可能是最好的方式.但这三种方法的原则非常重要,我认为这将帮助您从SQLAlchemy中获得最大价值.我希望这证明有用!