Παραμετροποιήσιμα Lookups¶
Το Django προσφέρει μια μεγάλη ποικιλία από προεγκατεστημένα lookups για το φιλτράρισμα (π.χ exact
και icontains
) των δεδομένων σας από την βάση δεδομένων. Το παρών εγχειρίδιο εξηγεί πως να γράψετε δικά σας lookups και πως να αλλάξετε τη συμπεριφορά των ήδη υπαρχουσών lookups. Για την σχετική αναφορά στο API των lookups, δείτε στο άρθρο Lookup API reference.
A lookup example¶
Let’s start with a small custom lookup. We will write a custom lookup ne
which works opposite to exact
. Author.objects.filter(name__ne='Jack')
will translate to the SQL:
"author"."name" <> 'Jack'
Η παραπάνω εντολή SQL είναι ανεξάρτητη από τη βάση δεδομένων που χρησιμοποιείτε, οπότε δεν θα χρειαστεί να ανησυχούμε γι” αυτό. Όλες οι βάσεις καταλαβαίνουν αυτή τη σύνταξη.
There are two steps to making this work. Firstly we need to implement the lookup, then we need to tell Django about it:
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
To register the NotEqual
lookup we will need to call register_lookup
on
the field class we want the lookup to be available for. In this case, the lookup
makes sense on all Field
subclasses, so we register it with Field
directly:
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'])
.
A transformer example¶
Το παραπάνω 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)
We can now run the queries we had before.
Experiment.objects.filter(change__abs=27)
will generate the following SQL:
SELECT ... WHERE ABS("experiments"."change") = 27
By using Transform
instead of Lookup
it means we are able to chain
further lookups afterwards. So
Experiment.objects.filter(change__abs__lt=27)
will generate the following
SQL:
SELECT ... WHERE ABS("experiments"."change") < 27
Σημειώστε ότι στην περίπτωση που δεν υπάρχει άλλο lookup στο τέλος, το Django μεταφράζει το change__abs=27
ως change__abs__exact=27
.
This also allows the result to be used in ORDER BY
and DISTINCT ON
clauses. For example Experiment.objects.order_by('change__abs')
generates:
SELECT ... ORDER BY ABS("experiments"."change") ASC
And on databases that support distinct on fields (such as PostgreSQL),
Experiment.objects.distinct('change__abs')
generates:
SELECT ... DISTINCT ON ABS("experiments"."change")
Για να βρει το 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
).
So we would like Experiment.objects.filter(change__abs__lt=27)
to generate
the following SQL:
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 συνάρτησης.
Let’s examine case-insensitive transformations here. This transformation isn’t very useful in practice as Django already comes with a bunch of built-in case-insensitive lookups, but it will be a nice demonstration of bilateral transformations in a database-agnostic way.
Ορίζουμε έναν 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)
Now, the queryset Author.objects.filter(name__upper="doe")
will generate a case
insensitive query like this:
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, **extra_context):
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')
.