Παραμετροποιήσιμα Lookups¶
Το Django προσφέρει μια μεγάλη ποικιλία από προεγκατεστημένα lookups για το φιλτράρισμα (π.χ exact
και icontains
) των δεδομένων σας από την βάση δεδομένων. Το παρών εγχειρίδιο εξηγεί πως να γράψετε δικά σας lookups και πως να αλλάξετε τη συμπεριφορά των ήδη υπαρχουσών lookups. Για την σχετική αναφορά στο API των lookups, δείτε στο άρθρο Lookup API reference.
Ένα απλό lookup παράδειγμα¶
Ας ξεκινήσουμε με ένα απλό παραμετροποιήσιμο lookup. Θα γράψουμε ένα δικό μας lookup και θα του δώσουμε το όνομα ne
(από τις λέξεις not equal), το οποίο θα έχει την ανάποδη συμπεριφορά του exact
. Για παράδειγμα, αν γράψουμε Author.objects.filter(name__ne='Jack')
, τότε αυτό θα μεταφραστεί στην SQL ως:
"author"."name" <> 'Jack'
Η παραπάνω εντολή SQL είναι ανεξάρτητη από τη βάση δεδομένων που χρησιμοποιείτε, οπότε δεν θα χρειαστεί να ανησυχούμε γι” αυτό. Όλες οι βάσεις καταλαβαίνουν αυτή τη σύνταξη.
Υπάρχουν δύο βήματα που πρέπει να κάνουμε για να δουλέψει το ανωτέρω. Πρώτα, πρέπει να υλοποιήσουμε το lookup και μετά πρέπει να το αναφέρουμε στο Django. Η υλοποίηση είναι λίγο πολύ ευθέως κατανοητή:
from django.db.models import Lookup
class NotEqual(Lookup):
lookup_name = 'ne'
def as_sql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return '%s <> %s' % (lhs, rhs), params
Σε αυτό το σημείο απλώς έχουμε ορίσει μια κλάση. Τίποτε περισσότερο. Επόμενο βήμα είναι κάνουμε register το νέο μας NotEqual
lookup με κάποιο πεδίο από τα Django μοντέλα ή με όλα. Θα χρειαστεί να καλέσουμε την register_lookup
πάνω στο πεδίο που θέλουμε αυτό το lookup να είναι διαθέσιμο. Αυτό γίνεται, γιατί σε μερικές περιπτώσεις δεν θέλουμε να έχουμε διαθέσιμο (να φιλτράρουμε, αν θέλετε) το lookup μας σε όλα τα πεδία (πχ IntegerField, DateField κλπ). Αν φτιάξουμε ένα άλλο lookup που να ελέγχει, πχ το πρώτο γράμμα ενός string δεν θα ήταν λογικό να το συνδέσουμε με ένα DateTimeField. Στην προκειμένη περίπτωση, το lookup μπορεί να εφαρμοστεί σε όλων των ειδών τα Field
subclasses, οπότε θα το κάνουμε register με το Field
απ” ευθείας:
from django.db.models.fields import Field
Field.register_lookup(NotEqual)
Το registration του lookup μπορεί να γίνει και με τη χρήση ενός decorator ως εξής:
from django.db.models.fields import Field
@Field.register_lookup
class NotEqualLookup(Lookup):
# ...
Τώρα μπορούμε να χρησιμοποιήσουμε το foo__ne
για κάθε τύπο πεδίου του foo
. Θα χρειαστεί να βεβαιωθείτε ότι το registration του lookup, γίνεται πριν προσπαθήσετε να δημιουργήσετε querysets χρησιμοποιώντας το. Θα μπορούσατε να τοποθετήσετε τον παραπάνω κώδικα μέσα στο αρχείο models.py
ή να κάνετε register το lookup μέσα στη μέθοδο ready()
στο αντίστοιχο AppConfig
της εφαρμογής σας.
Ρίχνοντας μια πιο προσεκτική ματιά στην υλοποίηση αυτού του lookup, το πρώτο απαραίτητο attribute που χρειάζεται είναι το lookup_name
. Αυτό επιτρέπει στο Django ORM να καταλάβει πως να μεταφράσει το name__ne
και να χρησιμοποιήσει την κλάση NotEqual
για να παράξει την SQL. Συμβατικά, αυτά τα ονόματα είναι πάντα πεζοί χαρακτήρες (μόνο γράμματα) και η μοναδική αυστηρή απαίτηση είναι να μην περιέχουν δύο συνεχόμενες κάτω παύλες, __
.
Έπειτα χρειάζεται να ορίσουμε την μέθοδο as_sql
. Αυτή δέχεται ως όρισμα ένα SQLCompiler
object, με το όνομα compiler
και την ενεργή σύνδεση της βάσης δεδομένων με το όνομα connection
. Τα SQLCompiler
objects δεν βρίσκονται κάπου γραμμένα στο εγχειρίδιο του Django αλλά το μόνο πράγμα που χρειάζεται να ξέρετε γι” αυτά είναι ότι έχουν μια μέθοδο compile()
η οποία επιστρέφει ένα tuple που περιέχει ένα SQL string και κάποιες παραμέτρους οι οποίες “γεμίζουν” αυτό το SQL string. Στις περισσότερες περιπτώσεις δεν θα χρειαστεί να την χρησιμοποιήσετε και μπορείτε να συνεχίσετε με τις μεθόδους process_lhs()
και process_rhs()
.
Ένα Lookup
συνεργάζεται με δύο τιμές, την lhs
και την rhs
, οι οποίες είναι σύντμηση των λέξεων left-hand side (αριστερό μέρος) και right-hand side (δεξί μέρος). Το αριστερό μέρος αναφέρεται συνήθως σε κάποιο πεδίο αλλά μπορεί να είναι οτιδήποτε αρκεί να έχει υλοποιήσει το API για εκφράσεις ερωτημάτων. Το δεξί μέρος είναι η τιμή η οποία δίνεται από τον χρήστη. Στο παράδειγμα Author.objects.filter(name__ne='Jack')
, το αριστερό μέρος αναφέρεται στο πεδίο name
του μοντέλου Author
και το string 'Jack'
αποτελεί το δεξί μέρος.
Καλούμε την process_lhs
και την process_rhs
για να μετατρέψουμε το Author.objects.filter(name__ne='Jack')
στις τιμές που χρειαζόμαστε για την SQL χρησιμοποιώντας το compiler
object που περιγράψαμε παραπάνω. Αυτές οι μέθοδοι επιστρέφουν tuples που περιέχουν λίγη SQL και κάποιες παραμέτρους που θα “γεμίσουν” αυτή την SQL. Ότι ακριβώς χρειαζόμαστε για να επιστρέψει η μέθοδος as_sql
. Στο παραπάνω παράδειγμα, η process_lhs
επιστρέφει ('"author"."name"', [])
και η process_rhs
επιστρέφει ('"%s"', ['Jack'])
. Όπως θα είδατε, δεν υπάρχουν παράμετροι για το αριστερό μέρος, αλλά αυτό εξαρτάται από την περίπτωση. Όπως και να έχει πάντως, θα πρέπει πάντα να περιλαμβάνονται στο τελικό tuple προς επιστροφή (η μεταβλητή params
στην as_sql
).
Στο τέλος ενώνουμε τα μέρη σε μια SQL εντολή με τη χρήση του <>
και παρέχουμε όλες τις παραμέτρους για το query (ερώτημα). Αυτό που επιστρέφουμε είναι ένα tuple που περιέχει την παραγόμενη SQL εντολή και τις παραμέτρους, κενές ή όχι. Το tuple που θα επιστραφεί, αναφερόμενοι στην ανωτέρω as_sql
, είναι το ('"author"."name" <> "%s"', ['Jack'])
.
Ένα απλό παράδειγμα transformer¶
Το παραπάνω lookup που φτιάξαμε είναι ωραίο, αλλά σε αρκετές περιπτώσεις θα θέλετε να έχετε τη δυνατότητα να ενώνετε τα lookups μεταξύ τους. Για παράδειγμα, ας υποθέσουμε ότι χτίζουμε μια εφαρμογή όπου θέλουμε να κάνουμε χρήση της μεθόδου abs()
, που επιστρέφει το απόλυτο ενός αριθμού. Έχουμε ένα μοντέλο με το όνομα Experiment
το οποίο καταγράφει μια τιμή έναρξης (start), μια τέλους (end) και την αλλαγή (αρχή – τέλος). Θα θέλαμε να βρούμε όλα τα πειράματα (experiments) όπου η αλλαγή ήταν ίση με μια συγκεκριμένη τιμή (Experiment.objects.filter(change__abs=27)
) ή η αλλαγή ήταν μικρότερη ή ίση μιας συγκεκριμένης τιμής (Experiment.objects.filter(change__abs__lt=27)
). Εδώ βλέπετε τη σύνδεση μεταξύ των lookups που αναφέραμε πριν (abs__lt
).
Σημείωση
Αυτό το παράδειγμα είναι λίγο πολύ σκηνοθετημένο αλλά επιδεικνύει όμορφα τις λειτουργίες που είναι δυνατόν να υλοποιηθούν χωρίς την εξάρτηση πάνω σε μια συγκεκριμένη βάση δεδομένων και χωρίς να ξαναγράφουμε κώδικα που υπάρχει ήδη στο Django.
We will start by writing an AbsoluteValue
transformer. This will use the SQL
function ABS()
to transform the value before comparison:
from django.db.models import Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
function = 'ABS'
Επόμενο βήμα είναι κάνουμε register την κλάση μας με το πεδίο IntegerField
:
from django.db.models import IntegerField
IntegerField.register_lookup(AbsoluteValue)
Μπορούμε τώρα να τρέξουμε τα queries στα οποία αναφερθήκαμε παραπάνω. Το Experiment.objects.filter(change__abs=27)
θα παράξει την ακόλουθη SQL εντολή:
SELECT ... WHERE ABS("experiments"."change") = 27
Χρησιμοποιώντας την Transform
αντί της Lookup
μας επιτρέπει να συνδέουμε περαιτέρω lookups στην αλυσίδα. Επομένως το Experiment.objects.filter(change__abs__lt=27)
θα παράξει την ακόλουθη SQL εντολή:
SELECT ... WHERE ABS("experiments"."change") < 27
Σημειώστε ότι στην περίπτωση που δεν υπάρχει άλλο lookup στο τέλος, το Django μεταφράζει το change__abs=27
ως change__abs__exact=27
.
Για να βρει το Django τον τύπο των lookups που επιτρέπονται μετά το δικό μας (AbsoluteValue
), κοιτάζει το attribute output_field
το οποίο δεν χρειάστηκε να το ορίσουμε στο συγκεκριμένο παράδειγμα γιατί έτυχε ο αριθμός που συγκρίνουμε, στο δεξί μέρος, να είναι ακέραιος (27). Αν μπερδευτήκατε, ας το ξεκαθαρίσουμε: Η Transform
προσφέρει τη μετατροπή ενός πεδίου (πχ του change
) σε κάποια άλλη τιμή (πχ από -27 που μπορεί να ήταν, σε 27) προτού συγκριθεί με το δεξί μέρος και γίνει το lookup στη βάση δεδομένων. Ο τύπος της νέας αυτής τιμής (πχ 27 αντί -27) δηλώνεται στο output_field
του δικού μας AbsoluteValue ``. Δεν το δηλώσαμε γιατί και οι δύο τιμές είναι τύπου int. Αν υποθέσουμε όμως ότι έχουμε πιο περίπλοκα πεδία (πχ καρτεσιανές συντεταγμένες ή μιγαδικούς αριθμούς) τότε ίσως θα θέλαμε να προσδιορίσουμε τον τύπο της νέας τιμής ως ``FloatField
για περαιτέρω lookups. Αυτό μπορεί να γίνει θέτοντας το attribute `` output_field`` ως property της δικής μας κλάσης, ως εξής:
from django.db.models import FloatField, Transform
class AbsoluteValue(Transform):
lookup_name = 'abs'
function = 'ABS'
@property
def output_field(self):
return FloatField()
Με αυτό τον τρόπο είμαστε σίγουροι ότι το δικό μας transformation θα επιστρέψει μια τιμή τύπου float και τα επόμενα lookups (αν υπάρχουν, πχ το lte στο abs__lte
) θα “δουν” την τιμή αυτή ως float και όχι ως int όπως ήταν στην αρχή. Ένα άλλο πολύ κοινό παράδειγμα της Transform
είναι η μετατροπή ενός DateField
στην τιμή του έτους μόνο. Για παράδειγμα, θα θέλατε να δείτε ποια πειράματα έγιναν το έτος 2016. Θα θέλατε να το γράψετε ως εξής: Experiment.objects.filter(date__year=2016)
. Το date
είναι το πεδίο του μοντέλου μας, τύπου DateField
και το year
το lookup (που υπάρχει ήδη μέσα στο Django). Το year
lookup “ξηλώνει” μόνο το έτος από την ημερομηνία και το συγκρίνει με το 2016. Έτσι, μετατρέπεται η ημερομηνία (DateField
) σε έτος (IntegerField
) και συγκρίνεται με το δεξί μέρος.
Γράφοντας ένα πιο αποτελεσματικό abs__lt
lookup¶
Όταν χρησιμοποιούμε το παραπάνω abs
lookup, η παραγόμενη SQL, σε μερικές περιπτώσεις, δεν χρησιμοποιεί αποτελεσματικά τα ευρετήρια (indexes) της βάσης δεδομένων. Πιο συγκεκριμένα, όταν χρησιμοποιούμε το change__abs__lt=27
, αυτό είναι ισοδύναμο με το change__gt=-27
AND change__lt=27
. (Στη περίπτωση του lte
θα μπορούσαμε να χρησιμοποιήσουμε την εντολή SQL BETWEEN
).
Θα θέλαμε, επομένως, η παραγόμενη SQL από το Experiment.objects.filter(change__abs__lt=27)
να δείχνει κάπως έτσι:
SELECT .. WHERE "experiments"."change" < 27 AND "experiments"."change" > -27
Η υλοποίηση αυτού φαίνεται παρακάτω:
from django.db.models import Lookup
class AbsoluteValueLessThan(Lookup):
lookup_name = 'lt'
def as_sql(self, compiler, connection):
lhs, lhs_params = compiler.compile(self.lhs.lhs)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params + lhs_params + rhs_params
return '%s < %s AND %s > -%s' % (lhs, rhs, lhs, rhs), params
AbsoluteValue.register_lookup(AbsoluteValueLessThan)
Υπάρχουν μερικά αξιοσημείωτα πράγματα εδώ. Πρώτα απ’ όλα το AbsoluteValueLessThan
δεν καλεί τη μέθοδο process_lhs()
. Αντιθέτως, υπερπηδά τη μετατροπή του lhs
που γίνεται από την AbsoluteValue
και χρησιμοποιεί την αρχική τιμή του lhs` (το self.lhs
είναι η τιμή μετά την μετατροπή ενώ το self.lhs.lhs
είναι η αρχική τιμή του change
). Αυτό γίνεται γιατί θέλουμε να έχουμε το "experiments"."change"
και όχι το ABS("experiments"."change")
. Είναι ασφαλής η αναφορά μας απ’ ευθείας στο self.lhs.lhs
γιατί η Lookup
κλάση AbsoluteValueLessThan
μπορεί να προσπελασθεί μόνο από την Transform
κλάση AbsoluteValue
(λόγω του register στην τελευταία γραμμή), δηλαδή το lhs
θα είναι πάντα ένα instance της Transform κλάσης AbsoluteValue
.
Επισημαίνεται επίσης το γεγονός ότι όχι μόνο και τα δύο μέρη (αριστερό και δεξί) χρησιμοποιούνται πολλές φορές μέσα στο SQL query αλλά και οι παράμετροι επίσης (lhs_params
και rhs_params
).
Ο λόγος που γίνεται αυτό είναι γιατί, σε περίπτωση που το self.rhs
είναι κάτι διαφορετικό από έναν ακέραιο (πχ μια αναφορά σε ένα F()
) δε θα μπορούμε να κάνουμε τις μετατροπές στην Python.
Σημείωση
Στην πραγματικότητα, τα περισσότερα lookups που εμπεριέχουν το __abs
θα μπορούσαν να υλοποιηθούν με την παραπάνω μέθοδο και στις περισσότερες βάσεις δεδομένων θα μπορέσετε να εκμεταλλευτείτε τη δυνατότητα των ευρετηρίων (indexes) τους. Ωστόσο, στην PostgreSQL ίσως θα θέλατε να προσθέσετε ένα ευρετήριο για το abs(change)
το οποίο θα επιτρέψει σε αυτά τα queries να είναι πολύ αποτελεσματικά.
Ένα παράδειγμα ενός διμερούς transformer¶
Το παράδειγμα της Transformer
κλάσης AbsoluteValue
που περιγράψαμε παραπάνω είναι μια μετατροπή η οποία εφαρμόζεται στο αριστερό μέρος του lookup (στο πεδίο change
). Ίσως να υπάρξουν, μερικές περιπτώσεις όπου θα θέλατε η μετατροπή να εφαρμοστεί και στο αριστερό και στο δεξί μέρος. Για παράδειγμα, αν θέλετε να φιλτράρετε ένα queryset βασιζόμενο στην ισότητα των δύο μερών (αριστερό και δεξί) με την χρήση κάποιας SQL συνάρτησης.
Ας εξετάσουμε την απλή περίπτωση μιας μετατροπής που δεν κάνει διάκριση μεταξύ πεζών και κεφαλαίων γραμμάτων. Αυτή η μετατροπή δεν είναι πολύ χρήσιμη στην πράξη καθώς το Django έχει προεγκατεστημένα αρκετά lookups που δεν κάνουν διακρίσεις μεταξύ πεζών-κεφαλαίων. Παρόλ’ αυτά αποτελεί ένα χρήσιμο παράδειγμα για την επίδειξη των διμερών μετατροπών (bilateral transformations) ανεξαρτήτως της βάση δεδομένων που χρησιμοποιείται.
Ορίζουμε έναν transformer UpperCase
ο οποίος χρησιμοποιεί την SQL συνάρτηση UPPER()
για να μετατρέψει τις τιμές πριν την σύγκριση. Θέτουμε το attribute bilateral = True
για να υποδείξουμε ότι η μετατροπή θα πρέπει να γίνει όχι μόνο στο αριστερό μέρος, lhs
, αλλά και στο δεξί, rhs
:
from django.db.models import Transform
class UpperCase(Transform):
lookup_name = 'upper'
function = 'UPPER'
bilateral = True
Έπειτα το κάνουμε register με τους ανάλογους τύπους πεδίων:
from django.db.models import CharField, TextField
CharField.register_lookup(UpperCase)
TextField.register_lookup(UpperCase)
Τώρα το queryset Author.objects.filter(name__upper="doe")
θα παράξει ένα query το οποίο δεν θα κάνει διάκριση μεταξύ πεζών-κεφαλαίων γραμμάτων:
SELECT ... WHERE UPPER("author"."name") = UPPER('doe')
Γράφοντας εναλλακτικές υλοποιήσεις για ήδη υπάρχοντα lookups¶
Κάποιες φορές, μερικές βάσεις δεδομένων απαιτούν έναν διαφορετικό τρόπο γραφής της SQL για την ίδια λειτουργία. Για παράδειγμα, κάποιες βάσεις για να προσδιορίσουν τη λογική «όχι ίσο με», αντί του συμβόλου <>
απαιτούν το !=
. (Στην πραγματικότητα σχεδόν όλες οι βάσεις δεδομένων υποστηρίζουν και τα δύο αυτά σύμβολα, συμπεριλαμβανομένων και όλων των επίσημων βάσεων δεδομένων που υποστηρίζει το Django). Σε αυτό το παράδειγμα θα ξαναγράψουμε μια δικιά μας υλοποίηση, για την MySQL, της λειτουργίας NotEqual.
We can change the behavior on a specific backend by creating a subclass of
NotEqual
with an as_mysql
method:
class MySQLNotEqual(NotEqual):
def as_mysql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return '%s != %s' % (lhs, rhs), params
Field.register_lookup(MySQLNotEqual)
Μπορούμε, έπειτα, να κάνουμε register την κλάση αυτή με το Field
. Η κλάση MySQLNotEqual
αντικαθιστά την κλάση NotEqual
, μόνο για τις MySQL βάσεις δεδομένων, αφού μοιράζονται το ίδιο lookup_name
.
Κατά τη διάρκεια μετάφρασης του query, το Django ψάχνει για τις μεθόδους``as_%s % connection.vendor`` και αν δεν βρει κάποια τότε χρησιμοποιεί την as_sql
. Τα ονόματα των vendor για τις προεγκατεστημένες στο Django database backends είναι τα sqlite
, postgresql
, oracle
και mysql
.
Πως προσδιορίζει το Django τα lookups και τα transforms που χρησιμοποιούνται¶
Σε μερικές περιπτώσεις θα θέλατε να αλλάξετε, δυναμικά, το Transform
ή το Lookup
που επιστρέφεται ανάλογα το όνομα που δίνεται στο query παρά να μπείτε στη διαδικασία να φτιάξετε ένα για το καθένα. Θα μπορούσατε, για παράδειγμα, να έχετε ένα πεδίο το οποίο αποθηκεύει συντεταγμένες ή κάποια αυθαίρετη διάσταση και θέλετε να μπορείτε να γράφετε το .filter(coords__x7=4)
και να σας επιστρέφει τα objects όπου η 7η συντεταγμένη έχει την τιμή 4. Για να το κάνετε αυτό, θα πρέπει να παρακάμψετε (override) την get_lookup
με κάτι σαν αυτό:
class CoordinatesField(Field):
def get_lookup(self, lookup_name):
if lookup_name.startswith('x'):
try:
dimension = int(lookup_name[1:])
except ValueError:
pass
else:
return get_coordinate_lookup(dimension)
return super().get_lookup(lookup_name)
Μετά θα ορίσετε τη μέθοδο get_coordinate_lookup
, καταλλήλως, για να επιστρέφει μια subclass της Lookup
η οποία θα χειριστεί την σχετική τιμή της μεταβλητής dimension
.
Ομοίως, υπάρχει μια μέθοδος για την Transform
με το όνομα get_transform()
. Η μέθοδος get_lookup()
θα πρέπει πάντα να επιστρέφει μια subclass της Lookup
ενώ η get_transform()
μια subclass της Transform
. Είναι σημαντικό να θυμάστε ότι τα objects της κλάσης Transform
μπορούν να φιλτραριστούν περαιτέρω ενώ τα objects της κλάσης Lookup
όχι.
Όταν φιλτράρετε και έχει απομείνει μόνο ένα lookup όνομα, το Django ψάξει για κάποιο Lookup
. Αν υπάρχουν πολλά τότε θα ψάξει για κάποιο Transform
. Στην περίπτωση που υπάρχει μόνο ένα όνομα και δεν βρεθεί κάποιο Lookup
, τότε το Django θα ψάξει για ένα Transform
και μετά το exact
lookup πάνω σε αυτό το Transform
. Στο τέλος, το Django, πάντα θα ψάχνει για ένα Lookup
. Για να ξεκαθαρίσουμε λίγο το τοπίο:
- Το
.filter(myfield__mylookup)
θα καλέσει την μέθοδοmyfield.get_lookup('mylookup')
. - Το
.filter(myfield__mytransform__mylookup)
θα καλέσει τη μέθοδοmyfield.get_transform('mytransform')
και μετά τηνmytransform.get_lookup('mylookup')
. - Το
.filter(myfield__mytransform)
θα καλέσει πρώτα τη μέθοδοmyfield.get_lookup('mytransform')
, η οποία θα αποτύχει. Έπειτα θα καλέσει την μέθοδοmyfield.get_transform('mytransform')
και τέλος τηνmytransform.get_lookup('exact')
.