web-dev-qa-db-fra.com

SQLAlchemy - Interrogation avec des colonnes DateTime pour filtrer par mois / jour / année

Je crée un site Web Flask qui implique de suivre les paiements, et j'ai rencontré un problème où je n'arrive pas vraiment à filtrer l'un de mes modèles de base de données par date.

Par exemple, si c'est à quoi ressemble ma table:

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

et je veux le filtrer de manière à obtenir toutes les lignes après le 20 juillet, ou toutes les lignes en août, etc.

Je peux penser à un moyen grossier et brutal de filtrer tous les paiements et ALORS parcourir la liste pour filtrer par mois/année, mais je préfère rester à l'écart de ces méthodes.

C'est mon modèle de paiement db:

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)

Et c'est moi qui essaie de filtrer Payment par date:

Payment.query.filter(Payment.due_date.month == today.month, Payment.due_date.year == today.year, Payment.due_date.day >= today.day).all()

today est simplement datetime.today().

J'ai supposé que la colonne due_date Aurait tous les attributs DateTime lorsque je l'appellerais (par exemple .month), Mais il semble que j'avais tort.

Quelle est la meilleure façon de filtrer les colonnes de Payment par date? Merci de votre aide.

7
LeetCoder

SQLAlchemy traduit efficacement votre requête exprimée en Python en SQL. Mais il le fait à un niveau relativement superficiel, en fonction du type de données que vous affectez au Column lors de la définition de votre modèle .

Cela signifie qu'il ne répliquera pas nécessairement l'API datetime.datetime De Python sur sa construction DateTime - après tout, ces deux classes sont censées faire des choses très différentes! (datetime.datetime Fournit une fonctionnalité datetime à Python, tandis que DateTime de SQLAlchemy indique à sa logique de traduction SQL qu'il s'agit d'une colonne SQL DATETIME ou TIMESTAMP).

Mais ne t'inquiète pas! Il y a plusieurs façons différentes de réaliser ce que vous essayez de faire, et certaines d'entre elles sont super faciles. Les trois plus faciles, je pense, sont:

  1. Construisez votre filtre en utilisant une instance complète de datetime, plutôt que ses composants (jour, mois, année).
  2. Utilisation de la construction extract de SQLAlchemy dans votre filtre.
  3. Définissez trois propriétés hybrides dans votre modèle qui renvoient le mois, le jour et l'année de paiement que vous pouvez ensuite filtrer.

Filtrage sur un objet datetime

C'est le plus simple des trois moyens (faciles) de réaliser ce que vous essayez, et il devrait également être le plus rapide. Fondamentalement, au lieu d'essayer de filtrer séparément chaque composant (jour, mois, année) dans votre requête, utilisez simplement une seule valeur datetime.

Fondamentalement, les éléments suivants devraient être équivalents à ce que vous essayez de faire dans votre requête ci-dessus:

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()

Maintenant, payments devrait être tous les paiements dont la date d'échéance survient après le début (heure 00:00:00) de la date actuelle de votre système.

Si vous voulez devenir plus compliqué, comme filtrer les paiements effectués au cours des 30 derniers jours. Vous pouvez le faire avec le code suivant:

from datetime import datetime, timedelta

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

payments = Payment.query.filter(Payment.due_date >= filter_after).all()

Vous pouvez combiner plusieurs cibles de filtre en utilisant and_ Et or_. Par exemple, pour retourner des paiements dus au cours des 30 derniers jours [~ # ~] et [~ # ~] étaient dus il y a plus de 15 ans, vous peut utiliser:

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()

L'astuce ici - de votre point de vue - est de construire correctement vos instances cible de filtre datetime avant d'exécuter votre requête.

Utilisation de la construction extract

L'expression extract de SQLAlchemy (documentée ici ) est utilisée pour exécuter une instruction SQL EXTRACT, qui est de savoir comment en SQL vous pouvez extraire un mois, un jour ou une année d'un Valeur DATETIME/TIMESTAMP.

En utilisant cette approche, SQLAlchemy indique à votre base de données SQL "d'abord, retirez le mois, le jour et l'année de ma colonne DATETIME et puis filtrez cette valeur extraite ". Sachez que cette approche sera plus lente que le filtrage sur une valeur datetime comme décrit ci-dessus. Mais voici comment cela fonctionne:

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()

Utilisation d'attributs hybrides

SQLAlchemy Attributs hybrides sont des choses merveilleuses. Ils vous permettent d'appliquer de manière transparente la fonctionnalité Python sans modifier votre base de données. Je soupçonne que pour ce cas d'utilisation spécifique, ils pourraient être exagérés, mais ils sont un troisième moyen d'atteindre ce que vous voulez.

Fondamentalement, vous pouvez considérer les attributs hybrides comme des "colonnes virtuelles" qui n'existent pas réellement dans votre base de données, mais que SQLAlchemy peut calculer à la volée à partir de vos colonnes de base de données lorsque cela est nécessaire.

Dans votre question spécifique, nous définirions trois propriétés hybrides: due_date_day, due_date_month, due_date_year Dans votre modèle Payment. Voici comment cela fonctionnerait:

... 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()

Voici ce que fait ce qui précède:

  1. Vous définissez votre modèle Payment comme vous le faites déjà.
  2. Mais vous ajoutez ensuite des attributs d'instance en lecture seule appelés due_date_year, due_date_month Et due_date_day. En utilisant due_date_year Comme exemple, il s'agit d'un attribut d'instance qui fonctionne sur les instances de votre classe Payment. Cela signifie que lorsque vous exécutez one_of_my_payments.due_date_year, La propriété extrait la valeur due_date De l'instance Python. Parce que tout cela se passe dans Python (c'est-à-dire sans toucher à votre base de données), il fonctionnera sur l'objet datetime.datetime déjà traduit que SQLAlchemy a stocké dans votre instance. Et il renverra le résultat de due_date.year.
  3. Ensuite, vous ajoutez un attribut de classe . C'est le bit qui est décoré de @due_date_year.expression. Ce décorateur indique à SQLAlchemy que lorsqu'il traduit des références à due_date_year En expressions SQL, il doit le faire comme défini dans cette méthode. Ainsi, l'exemple ci-dessus indique à SQLAlchemy "si vous devez utiliser due_date_year Dans une expression SQL, alors extract('year', Payment.due_date) est la façon dont due_date_year Doit être exprimé.

(Remarque: l'exemple ci-dessus suppose que due_date_year, due_date_month et due_date_day sont tous des propriétés en lecture seule. Vous pouvez bien sûr également définir des paramètres personnalisés à l'aide de @due_date_year.setter qui accepte également les arguments (self, value))

En conclusion

De ces trois approches, je pense que la première approche (filtrage sur datetime) est à la fois la plus facile à comprendre, la plus facile à implémenter et la plus performante. C'est probablement la meilleure façon de procéder. Mais les principes de ces trois approches sont très importants et je pense que cela vous aidera à tirer le meilleur parti de SQLAlchemy. J'espère que cela s'avère utile!

16
Chris Modzelewski