Γράφοντας δικά σας πεδία μοντέλων

Εισαγωγή

Το άρθρο μοντέλα εξηγεί πως να χρησιμοποιήσετε τις στάνταρντ κλάσεις πεδίων του Django – CharField, DateField κλπ. Στις περισσότερες περιπτώσεις αυτές οι κλάσεις θα σας είναι αρκετές. Ωστόσο, η έκδοση του Django που χρησιμοποιείτε ίσως να μην καλύπτει τις ανάγκες και τις απαιτήσεις σας ή θα θέλατε να χρησιμοποιήσετε ένα πεδίο μοντέλου το οποίο είναι εντελώς διαφορετικό από αυτά που έρχονται με το Django.

Οι τύποι των προεγκατεστημένων πεδίων του Django δεν καλύπτουν όλους τους πιθανούς τύπους στηλών μιας βάσης δεδομένων (υπόψιν ότι μερικές βάσεις έχουν αποκλειστικά δικούς τους τύπους στηλών που δεν βρίσκονται σε κάποια άλλη βάση) – μόνο τους βασικούς τύπους, όπως VARCHAR και INTEGER. Για περισσότερους τύπους στηλών, όπως γεωγραφικά πολύγονα ή ακόμη και τύπους που μπορεί να δημιουργήσει ο χρήστης όπως οι PostgreSQL custom types, μπορείτε να ορίσετε τις δικές σας Django Field subclasses.

Εναλλακτικά, μπορείτε να έχετε ένα πολύπλοκο Python object το οποίο να μπορεί κάπως να γίνει serialized για να μπορέσει να αναπαρασταθεί ως στάνταρντ στήλη στη βάση δεδομένων. Αυτή είναι μια άλλη περίπτωση όπου μια subclass της Field θα σας βοηθήσει να χρησιμοποιήσετε το object σας με τα μοντέλα σας.

Το object του παραδείγματος μας

Η δημιουργία δικών μας παραμετροποιήσιμων πεδίων απαιτεί προσοχή στη λεπτομέρεια. Για να κάνουμε τα πράγματα πιο εύκολα, θα χρησιμοποιήσουμε το ίδιο παράδειγμα σε όλο αυτό το άρθρο: θα δημιουργήσουμε ένα Python object που θα αναπαριστά το μοίρασμα των χαρτιών σε μια παρτίδα Bridge. Μην ανησυχείτε, όμως, δεν χρειάζεται να γνωρίζετε πως παίζεται το Bridge για να καταλάβετε αυτό το παράδειγμα. Το μόνο που χρειάζεται να ξέρετε είναι ότι απαιτούνται 52 χαρτιά να μοιραστούν ισομερώς σε τέσσερις παίκτες, οι οποίοι ονομάζονται, παραδοσιακά, north, east, south και west. Η κλάση μας θα μοιάζει κάπως έτσι:

class Hand:
    """A hand of cards (bridge style)"""

    def __init__(self, north, east, south, west):
        # Input parameters are lists of cards ('Ah', '9s', etc.)
        self.north = north
        self.east = east
        self.south = south
        self.west = west

    # ... (other possibly useful methods omitted) ...

Η κλάση που γράψαμε δεν είναι τίποτε άλλο παρά μια συνηθισμένη κλάση Python, χωρίς να την συνδέει κάτι με το Django. Θα θέλαμε να κάνουμε πράγματα, όπως αυτό, στα Django μοντέλα μας (υποθέτουμε ότι το attribute hand του μοντέλου είναι ένα instance της κλάσης Hand):

example = MyModel.objects.get(pk=1)
print(example.hand.north)

new_hand = Hand(north, east, south, west)
example.hand = new_hand
example.save()

Εκχωρούμε στο και ανακτούμε από το attribute hand του μοντέλου μας όπως ακριβώς θα κάναμε με κάθε άλλη κλάση της Python. Το κόλπο είναι να πούμε στο Django πως να διαχειριστεί την αποθήκευση στην και τη φόρτωση από τη βάση δεδομένων αυτού του object.

Για να χρησιμοποιήσουμε την κλάση Hand στα μοντέλα μας, δεν χρειάζεται να αλλάξουμε αυτή την κλάση καθόλου. Αυτό είναι ιδανικό, καθώς σημαίνει ότι μπορείτε να γράψετε Django μοντέλα για ήδη υπάρχουσες κλάσεις χωρίς να χρειαστεί να πειράξετε τον πηγαίο κώδικα.

Σημείωση

Ίσως να θέλετε, μόνο, να επωφεληθείτε από τους δικούς σας τύπους στηλών στη βάση δεδομένων και να διαχειριστείτε τα δεδομένα σας ως στάνταρντ τύπους Python objects στα μοντέλα σας, πχ strings, floats κλπ. Αυτή η περίπτωση είναι παρόμοια με το παράδειγμα μας, Hand, όπου θα υποδεικνύουμε τυχόν διαφορές καθώς προχωράμε.

Βασική θεωρία

Αποθηκευτικός χώρος στη βάση δεδομένων

Ο πιο απλός τρόπος για να σκεφτείτε έναν τύπο μοντέλου (model field) είναι ότι προσφέρει έναν τρόπο ενός συνηθισμένου Python object – string, boolean, datetime, ή κάτι πιο περίπλοκο όπως το Hand – να μετατραπεί σε και από μια μορφή η οποία είναι χρήσιμη όταν επικοινωνούμε με τη βάση δεδομένων (και με το serialization, αλλά, όπως θα δούμε αργότερα, αυτό δεν θα σας απασχολήσει όταν έχετε τακτοποιήσει τη βάση).

Τα πεδία σε ένα μοντέλο πρέπει με κάποιο τρόπο να μετατραπούν ούτως ώστε να μπορέσει να τα δεχτεί ο αντίστοιχος τύπος στήλης της βάσης δεδομένων. Διαφορετικές βάσεις δεδομένων προσφέρουν διαφορετικά σετ από έγκυρους τύπους στηλών, αλλά ο κανόνας είναι ο ίδιος: αυτοί είναι οι μοναδικοί τύποι με τους οποίους θα μπορείτε να δουλεύετε. Οτιδήποτε θέλετε να αποθηκεύσετε στη βάση δεδομένων, πρέπει να προσαρμοστεί σε έναν από αυτούς τους τύπους.

Συνήθως, είτε θα γράφετε ένα πεδίο Django για να ταιριάζει με έναν συγκεκριμένο τύπο στήλης της βάσης είτε θα μετατρέπετε σε μορφή string, με κάποιον τρόπο, τα δεδομένα προς αποθήκευση στη βάση.

Για το δικό μας παράδειγμα Hand, θα μπορούσαμε να μετατρέψουμε τα δεδομένα των χαρτιών της τράπουλας σε ένα string 104 χαρακτήρων ενώνοντας (concatenating) όλα τα χαρτιά μεταξύ τους σε μια προκαθορισμένη σειρά – πχ, όλα τα north χαρτιά πρώτα, μετά τα east, μετά τα south και τέλος τα west. Επομένως, τα objects τύπου Hand θα μπορούσαν να αποθηκευτούν ως στήλη κειμένου (text) ή ως στήλη χαρακτήρων (character) στη βάση δεδομένων. Για άλλη μια φορά, επειδή η βάση δεν καταλαβαίνει την γλώσσα Python, θα πρέπει να μετατρέψουμε τα δεδομένα μας σε κάποια μορφή που η βάση καταλαβαίνει. Αντίστροφα, όταν αντλούμε δεδομένα από τη βάση, θα πρέπει να τα μετατρέψουμε σε Python objects για να τα διαχειριστούμε, ανάλογα τις ανάγκες μας.

Τι κάνει μια κλάση τύπου Field;

Όλα τα πεδία του Django (και όταν λέμε πεδία σε αυτό το άρθρο, εννοούμε πάντα πεδία μοντέλων και όχι πεδία φορμών) είναι subclasses της κλάσης django.db.models.Field. Οι περισσότερες πληροφορίες που καταγράφει το Django σχετικά με ένα πεδίο είναι κοινές για όλα τα πεδία – όνομα, βοηθητικό κείμενο, μοναδικότητα κοκ. Η αποθήκευση όλων αυτών των πληροφοριών γίνεται από την κλάση Field. Θα πούμε με λεπτομέρειες τι μπορεί να κάνει η Field σε λίγο. Για τώρα, αρκεί να πούμε ότι όλα τα πεδία κληρονομούν από την κλάση Field και παραμετροποιούν διάφορα βασικά κομμάτια της συμπεριφοράς της.

Είναι σημαντικό να συνειδητοποιήσετε ότι η κλάση ενός πεδίου του Django (πχ CharField, BooleanField κλπ) δεν αποθηκεύεται στα attributes των μοντέλων σας. Τα attributes του μοντέλου περιέχουν συνηθισμένα Python objects. Η κλάσεις των πεδίων που ορίζετε σε ένα μοντέλο αποθηκεύονται στην ουσία σε μια Meta κλάση όταν δημιουργείται το μοντέλο (οι ακριβείς λεπτομέρειες του πως γίνεται αυτό, δεν έχουν σημασία τώρα). Αυτό γίνεται επειδή οι κλάσεις των πεδίων δεν είναι απαραίτητες όταν απλώς δημιουργείτε και αλλάζετε τα attributes. Αντ’ αυτού οι κλάσεις προσφέρουν τον μηχανισμό για την μετατροπή μεταξύ του attribute και αυτού που είναι αποθηκευμένο στη βάση δεδομένων ή για την αποστολή στον serializer.

Κρατήστε στο μυαλό σας τα ακόλουθα, όταν δημιουργείτε δικά σας πεδία. Η subclass Field του Django που γράφετε, σας προσφέρει έναν μηχανισμό μετατροπής μεταξύ Python instances και τιμών database/serializer με διάφορους τρόπους (πχ, υπάρχουν διαφορές μεταξύ της αποθήκευσης της τιμής και της χρησιμοποίησης της στα lookups). Αν αυτό ακούγεται κάπως περίπλοκο, μην ανησυχείτε – θα γίνει κατανοητό στα παραδείγματα παρακάτω. Να θυμάστε, μόνο, ότι συχνά θα καταλήγετε με τη δημιουργία δύο κλάσεων, όταν γράφετε δικά σας πεδία:

  • Η πρώτη κλάση θα είναι το Python object που θα χρησιμοποιούν οι χρήστες. Θα το αναθέσουν σε ένα attribute του μοντέλου, θα διαβάζουν από αυτό για να βλέπουν τα δεδομένα που αντιστοιχούν σε αυτό, θα αποθηκεύουν δεδομένα μέσω αυτού κλπ. Αυτή η κλάση είναι η Hand στο παράδειγμα μας.
  • Η δεύτερη κλάση θα είναι μια subclass της Field. Αυτή είναι μια κλάση η οποία ξέρει πως να μετατρέψει την πρώτη σας κλάση σε δεδομένα που καταλαβαίνει η βάση δεδομένων και αντίστροφα τα δεδομένα που είναι αποθηκευμένα στη βάση δεδομένων σε object που καταλαβαίνει η Python.

Γράφοντας μια subclass ενός πεδίου

Όταν σχεδιάζετε τη subclass της κλάσης Field (το δικό σας νέο πεδίο, αν θέλετε), θα πρέπει πρώτα να σκεφτείτε με ποια από τις ήδη υπάρχουσες κλάσεις Field το νέο σας πεδίο μοιάζει περισσότερο. Ίσως να μπορείτε να κάνετε subclass ένα ήδη υπάρχον Django πεδίο και να γλυτώσετε τον εαυτό σας πολύτιμο χρόνο. Αν όχι, θα πρέπει να κάνετε subclass την κλάση Field, απ” όπου όλα κληρονομούν.

Η αρχικοποίηση (initialization) του νέου σας πεδίου έχει να κάνει με τον διαχωρισμό των arguments που είναι συγκεκριμένα για αυτό που θέλετε να κάνει το πεδίο σας και των συνηθισμένων arguments. Στην μέθοδο __init__() της κλάσης Field (ή στην parent κλάση) περνάτε τα arguments που είναι συγκεκριμένα για το πεδίο σας.

Στο παράδειγμα μας, θα ονομάσουμε το πεδίο μας HandField. (Είναι, γενικά, καλή πρακτική να καλείτε τη subclass της Field ως <Something>Field, ούτως ώστε να εύκολα αναγνωρίσιμη ως μια subclass της Field). Η HandField δεν συμπεριφέρεται ως ένα ήδη υπάρχον Django πεδίο (πχ CharField ή TextField), οπότε θα κάνουμε subclass απ’ ευθείας από την Field:

from django.db import models

class HandField(models.Field):

    description = "A hand of cards (bridge style)"

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super().__init__(*args, **kwargs)

Η HandField δέχεται τις περισσότερες από τις στάνταρντ επιλογές πεδίων (δείτε τη λίστα παρακάτω), αλλά εδώ εξασφαλίζουμε ότι έχει ένα συγκεκριμένο μέγιστο μήκος, αφού το μέγιστο που χρειάζεται είναι 52 τιμές φύλλων μαζί με το χρώμα του καθενός, πχ άσσος μπαστούνι (As, ace of spades) ή τρία κούπα (3h, 3 of hearts) κλπ: 104 χαρακτήρες στο σύνολο.

Σημείωση

Πολλά από τα πεδία των μοντέλων του Django δέχονται μερικές επιλογές με τις οποίες δεν κάνουν τίποτα, απλώς τις αγνοούν. Για παράδειγμα, μπορείτε να περάσετε την παράμετρο editable και την auto_now σε μια κλάση τύπου django.db.models.DateField και η παράμετρος editable απλά θα αγνοηθεί (όταν το auto_now είναι True προϋποθέτει ότι editable=False). Κανένα σφάλμα δεν θα γίνει raised σε αυτή την περίπτωση.

Αυτή η συμπεριφορά απλοποιεί τις κλάσεις των πεδίων επειδή δεν χρειάζεται να ελέγχουν για επιλογές (παραμέτρους) οι οποίες δεν είναι απαραίτητες. Απλώς, περνούν όλες τις επιλογές στην parent class και στη συνέχεια δεν τις χρησιμοποιούν. Από σας εξαρτάται αν θέλετε τα πεδία σας να είναι αυστηρά (strict) σε σχέση με τις παραμέτρους που επιλέγουν, ή να είναι πιο απλά, δηλαδή να δέχονται περισσότερες επιλογές.

Η μέθοδος Field.__init__() δέχεται τις ακόλουθες παραμέτρους:

  • verbose_name
  • name
  • primary_key
  • max_length
  • unique
  • blank
  • null
  • db_index
  • rel: Χρησιμοποιείται για συσχετισμένα πεδία (όπως το ForeignKey). Για προχωρημένους χρήστες μόνο.
  • default
  • editable
  • serialize: Αν είναι False, το πεδίο δεν θα γίνει serialized όταν το μοντέλο περάσει στους serializers του Django. Η προεπιλεγμένη τιμή είναι True.
  • unique_for_date
  • unique_for_month
  • unique_for_year
  • choices
  • help_text
  • db_column
  • db_tablespace: Μόνο για τη δημιουργία περιεχομένων (indexes), αν η βάση δεδομένων υποστηρίζει τα tablespaces. Τις πιο πολλές φορές, μπορείτε να αγνοείτε αυτή την επιλογή.
  • auto_created: Είναι True αν το πεδίο δημιουργήθηκε αυτόματα, όπως το OneToOneField που χρησιμοποιείται από την κληρονομικότητα των μοντέλων. Για προχωρημένους χρήστες μόνο.

Όλες οι επιλογές χωρίς εξήγηση, στην ανωτέρω λίστα, έχουν την ίδια έννοια με τα συνηθισμένα πεδία του Django. Δείτε στο άρθρο εγχειρίδιο πεδίων των μοντέλων για παραδείγματα και λεπτομέρειες.

Deconstruction του πεδίου

Όταν υλοποιείτε τη μέθοδο __init__() θα πρέπει να υλοποιήσετε και τη μέθοδο deconstruct(). Αυτή η μέθοδος λέει στο Django πως να μετατρέψει το instance του καινούργιου σας πεδίου σε μια σειριακή μορφή (serialized form) - πιο συγκεκριμένα, ποια arguments θα πρέπει να περαστούν στη μέθοδο __init__() για να ξαναφτιαχτεί το instance.

Αν δεν έχετε προσθέσει τυχόν έξτρα επιλογές στη μέθοδο __init__() του δικού σας πεδίου, δεν χρειάζεται να γράψετε κάποια νέα μέθοδο deconstruct(). Αν, ωστόσο, αλλάξετε τα arguments που περνούν στην __init__() (όπως κάναμε στην κλάση HandField που αλλάξαμε το kwargs['max_length']), θα χρειαστεί να προσθέσετε τις τιμές που περάσατε.

Το συμβόλαιο με την μέθοδο deconstruct() είναι απλό. Επιστρέφει ένα tuple τεσσάρων στοιχείων: το όνομα του attribute του field, το πλήρες import path της κλάσης του πεδίου, τα positional arguments (ως λίστα) και τα keyword arguments (ως dictionary). Σημειώστε ότι αυτή η μέθοδος είναι διαφορετική από τη μέθοδο deconstruct() όταν γράφεται δικές σας κλάσεις η οποία επιστρέφει ένα tuple τριών στοιχείων.

Ως συντάκτης του δικού σας πεδίου, δεν θα σας απασχολήσουν τα δύο πρώτα στοιχεία του tuple. Η κύρια κλάση της Field έχει όλο τον απαραίτητο κώδικα για να υπολογίσει το όνομα και το import path του πεδίου σας. Ωστόσο, σίγουρα θα σας απασχολήσουν τα positional και keyword arguments, αφού αυτά είναι τα στοιχεία που πιθανόν να αλλάξετε.

Για παράδειγμα, στην κλάση HandField θέτουμε το max_length μέσα στην __init__(). Η μέθοδος deconstruct() της κλάσης Field (την οποία καλούμε - super(HandField, self).deconstruct()) θα δει ότι έχει οριστεί το max_length και θα το επιστρέψει μέσα στο kwargs dictionary. Για λόγους αναγνωσιμότητας μπορούμε να το διαγράψουμε από το dictionary και έτσι το kwargs να είναι κενό. Όταν, επομένως, πάρουμε αυτό το tuple των τεσσάρων στοιχείων και προσπαθήσουμε να χτίσουμε ένα instance του HandField δεν έχει πολύ νόημα το kwargs να περιέχει το max_length αφού κατά το χτίσιμο του instance θα κληθεί η __init__ η οποία το ορίζει μέσα της:

from django.db import models

class HandField(models.Field):

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]
        return name, path, args, kwargs

Αν προσθέσετε ένα νέο keyword argument, θα χρειαστεί να γράψετε κώδικα για να προσθέσετε την τιμή του στο kwargs:

from django.db import models

class CommaSepField(models.Field):
    "Implements comma-separated storage of lists"

    def __init__(self, separator=",", *args, **kwargs):
        self.separator = separator
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        # Only include kwarg if it's not the default
        if self.separator != ",":
            kwargs['separator'] = self.separator
        return name, path, args, kwargs

Πιο περίπλοκα παραδείγματα είναι πέρα από τους σκοπούς αυτού του άρθρου, αλλά να θυμάστε – για κάθε παραμετροποίηση του instance του Field, η μέθοδος deconstruct() πρέπει να επιστρέφει arguments τα οποία αν περάσετε στην __init__ θα ξαναφτιάξετε το πεδίο σας.

Δώστε μεγάλη προσοχή στην περίπτωση που δώσετε τυχόν νέες προεπιλεγμένες τιμές στα arguments της superclass της Field. Πρέπει να εξασφαλίσετε ότι πάντοτε περιλαμβάνονται παρά εξαφανίζονται όταν λαμβάνουν την παλιά προεπιλεγμένη τιμή.

Επιπροσθέτως, προσπαθήστε να αποφύγετε να περάσετε τιμές ως positional arguments, όπου δυνατόν και αντί αυτού να επιστρέφετε τις τιμές ως keyword arguments για περισσότερη συμβατότητα στο μέλλον. Φυσικά, αν αλλάζετε τα ονόματα πιο συχνά από τη θέση τους στη λίστα των arguments του constructor, ίσως να προτιμήσετε τα positional arguments, αλλά έχετε στο μυαλό σας ότι άλλοι προγραμματιστές θα κάνουν reconstruct το πεδίο σας από την σειριακή έκδοση του για αρκετό καιρό (πιθανόν χρόνια), αναλόγως το χρόνο ζωής των migration σας.

Μπορείτε να δείτε τα αποτελέσματα του deconstruction κοιτάζοντας τα migrations τα οποία περιέχουν το πεδίο και μπορείτε, επίσης, να τεστάρετε το deconstruction σε unit tests απλώς κάνοντας deconstruct και reconstruct το πεδίο:

name, path, args, kwargs = my_field_instance.deconstruct()
new_instance = MyField(*args, **kwargs)
self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)

Αλλάζοντας την base class του δικού σας πεδίου

Δεν μπορείτε να αλλάξετε την base class ενός παραμετροποιήσιμου πεδίου επειδή το Django δεν ανιχνεύει την αλλαγή και δεν θα δημιουργήσει migration για αυτή την αλλαγή. Για παράδειγμα αν ξεκινήσετε με αυτό:

class CustomCharField(models.CharField):
    ...

και μετά αποφασίσετε ότι θέλετε να χρησιμοποιήσετε αντί αυτού ένα TextField, δεν μπορείτε να αλλάξετε την subclass όπως:

class CustomCharField(models.TextField):
    ...

Αντιθέτως, πρέπει να δημιουργήσετε μια νέα κλάση πεδίου και να ενημερώσετε τα μοντέλα σας για να αναφερθούν σε αυτό:

class CustomCharField(models.CharField):
    ...

class CustomTextField(models.TextField):
    ...

Όπως συζητήθηκε στο αφαιρώντας πεδία μοντέλων, θα πρέπει να διατηρήσετε την αρχική κλάση CustomCharField στον κώδικα σας όσο έχετε migrations τα οποία αναφέρονται σε αυτή.

Τεκμηριώνοντας το δικό σας πεδίο

Όπως πάντα, θα πρέπει να τεκμηριώνετε (περιγραφικά και βοηθητικά σχόλια) τον τύπο του πεδίου σας, προκειμένου οι χρήστες αυτού να γνωρίζουν περί τίνος πρόκειται. Πέρα από το γεγονός του να γράψετε ένα docstring για αυτό, το οποίο είναι χρήσιμο για τους developers, μπορείτε επίσης να προσφέρετε στους χρήστες του Django admin να δουν μια μικρή περιγραφή του τύπου του πεδίου μέσω της εφαρμογής django.contrib.admindocs. Για να γίνει αυτό απλώς προσθέστε ένα περιγραφικό κείμενο στο class attribute description του δικού σας πεδίου. Στο παραπάνω παράδειγμα, η περιγραφή που εμφανίζεται από την εφαρμογή admindocs για το πεδίο HandField θα είναι η “A hand of cards (bridge style)”.

Στην εμφάνιση του module django.contrib.admindocs, η περιγραφή του πεδίου ενσωματώνεται στο field.__dict__ το οποίο επιτρέπει στην περιγραφή να συνεργαστεί με arguments του πεδίου. Για παράδειγμα, η περιγραφή του CharField είναι η:

description = _("String (up to %(max_length)s)")

Χρήσιμες μέθοδοι

Όταν φτιάξετε την subclass της κλάσης Field, μπορείτε, αν θέλετε, να παρακάμψετε (override) μερικές στάνταρντ μεθόδους, ανάλογα τη συμπεριφορά του πεδίου σας. Η λίστα με τις μεθόδους, παρακάτω, είναι περίπου κατά φθίνουσα σειρά σπουδαιότητας, οπότε ξεκινήστε από πάνω προς τα κάτω.

Δικά σας πεδία βάσης δεδομένων

Ας υποθέσουμς ότι έχετε δημιουργήσει ένα δικό σας πεδίο στη PostgreSQL με το όνομα mytype. Μπορείτε να κάνετε subclass τη κλάση Field και να υλοποιήσετε τη μέθοδο db_type(), ως εξής:

from django.db import models

class MytypeField(models.Field):
    def db_type(self, connection):
        return 'mytype'

Όταν θα έχετε γράψει το MytypeField, μπορείτε να το χρησιμοποιήσετε σε οποιοδήποτε από τα μοντέλα σας, όπως ακριβώς με οποιοδήποτε άλλο τύπο Field:

class Person(models.Model):
    name = models.CharField(max_length=80)
    something_else = MytypeField()

Αν σκοπεύετε να φτιάξετε μια εφαρμογή η οποία είναι ανεξάρτητη από τη βάση δεδομένων που χρησιμοποιείται, θα πρέπει να λάβετε υπόψη σας τις διαφορές τύπων στηλών μεταξύ των βάσεων δεδομένων. Για παράδειγμα, η στήλη τύπου date/time στην PostgreSQL καλείτε timestamp, ενώ η ίδια στήλη στη MySQL καλείται datetime. Ο πιο απλός τρόπος να χειριστείτε αυτή την κατάσταση μέσα στη μέθοδο db_type() είναι να ελέγξετε το attribute connection.settings_dict['ENGINE'].

Για παράδειγμα:

class MyDateField(models.Field):
    def db_type(self, connection):
        if connection.settings_dict['ENGINE'] == 'django.db.backends.mysql':
            return 'datetime'
        else:
            return 'timestamp'

Οι μέθοδοι db_type() και rel_db_type() καλούνται από το Django όταν το framework φτιάχνει τα CREATE TABLE statements για την εφαρμογή σας – δηλαδή, στο αρχικό στάδιο δημιουργίας των πινάκων σας. Οι μέθοδοι, καλούνται, επίσης, όταν δημιουργείται μια WHERE εντολή η οποία περιλαμβάνει το πεδίο του μοντέλου – δηλαδή, όταν αντλείτε δεδομένα χρησιμοποιώντας μεθόδους QuerySet όπως η get(), filter() και exclude() και έχετε περασμένο το πεδίο του μοντέλου ως argument. Οι μέθοδοι αυτές δεν καλούνται κάποια άλλη στιγμή, οπότε αξίζει να εκτελέσουν έναν ελαφρά περίπλοκο κώδικα, όπως το να ελέγξουν το connection.settings_dict όπως στο παραπάνω παράδειγμα.

Μερικοί τύποι στηλών βάσης δεδομένων δέχονται παραμέτρους, όπως CHAR(25), όπου η παράμετρος 25 αναπαριστά το μέγιστο μήκος χαρακτήρων της στήλης. Σε παρόμοιες περιπτώσεις, είναι πιο ευέλικτο η παράμετρος να δηλωθεί στο μοντέλο (ως max_length attribute) παρά να γραφεί με το χέρι μέσα στη μέθοδο db_type(). Για παράδειγμα, δεν θα είχε πολύ νόημα να είχατε ένα πεδίο CharMaxlength25Field, όπως φαίνεται εδώ:

# This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field):
    def db_type(self, connection):
        return 'char(25)'

# In the model:
class MyModel(models.Model):
    # ...
    my_field = CharMaxlength25Field()

Ένας καλύτερος τρόπος είναι να προσδιορίσετε την παράμετρο κατά τη διάρκεια του run time – πχ, όταν η κλάση αρχικοποιείται. Για να το κάνετε αυτό, απλώς υλοποιήστε την Field.__init__(), ως εξής (εδώ περνάμε την παράμετρο max_length ως positional argument):

# This is a much more flexible example.
class BetterCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super().__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(%s)' % self.max_length

# In the model:
class MyModel(models.Model):
    # ...
    my_field = BetterCharField(25)

Εν τέλει, αν η στήλη σας απαιτεί πραγματικά περίπλοκο SQL στήσιμο, επιστρέψτε None από την μέθοδο db_type(). Αυτό θα επιτρέψει στον κώδικα του Django που παράγει την SQL να προσπεράσει αυτό το πεδίο. Είστε, πλέον, υπεύθυνοι να δημιουργήσετε εσείς τη στήλη στο σωστό πίνακα με κάποιον άλλο τρόπο. Αυτό, φυσικά, λέει στο Django να βγει από τη μέση.

Η μέθοδος rel_db_type() καλείται από πεδία τύπου ForeignKey και OneToOneField τα οποία δείχνουν σε ένα άλλο πεδίο για να καθορίσουν τον τύπο δεδομένων της στήλης. Για παράδειγμα, αν έχετε ένα πεδίο τύπου UnsignedAutoField, χρειάζεστε επίσης τα foreign keys τα οποία δείχνουν σε αυτό το πεδίο για να χρησιμοποιηθεί ο ίδιος τύπος δεδομένων:

# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
    def db_type(self, connection):
        return 'integer UNSIGNED AUTO_INCREMENT'

    def rel_db_type(self, connection):
        return 'integer UNSIGNED'

Μετατρέποντας τιμές σε Python objects

Αν η δικιά σας κλάση Field ασχολείται με δομές δεδομένων (data structures) οι οποίες είναι πιο περίπλοκες από strings, dates, integers, ή floats, τότε ίσως χρειαστεί να παρακάμψετε (override) τις μέθοδους from_db_value() και to_python().

Αν η μέθοδος from_db_value() έχει υλοποιηθεί στη subclass του πεδίου, τότε θα καλείται σε όλες τις περιπτώσεις που τα δεδομένα φορτώνονται από τη βάση δεδομένων, συμπεριλαμβανομένων των κλήσεων των aggregate και values().

Η μέθοδος to_python() καλείται κατά το deserialization και κατά τη διάρκεια της μεθόδου clean() που χρησιμοποιείται από τις φόρμες.

Ως γενικός κανόνας, η μέθοδος to_python() θα πρέπει συνεργάζεται με οποιοδήποτε από τα ακόλουθα arguments:

  • Ένα instance ενός σωστού τύπου (πχ, του Hand στο τρέχων παράδειγμα μας).
  • Ένα string
  • None (αν το πεδίο επιτρέπει null=True)

Στην κλάση HandField, αποθηκεύουμε τα δεδομένα ως ένα πεδίο VARCHAR στη βάση δεδομένων, οπότε θα πρέπει να είμαστε σε θέση να επεξεργαστούμε τύπου strings και τύπου None στη μέθοδο from_db_value(). Στη μέθοδο to_python(), θα πρέπει, επίσης, να δουλέψουμε με instances της κλάσης Hand:

import re

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _

def parse_hand(hand_string):
    """Takes a string of cards and splits into a full hand."""
    p1 = re.compile('.{26}')
    p2 = re.compile('..')
    args = [p2.findall(x) for x in p1.findall(hand_string)]
    if len(args) != 4:
        raise ValidationError(_("Invalid input for a Hand instance"))
    return Hand(*args)

class HandField(models.Field):
    # ...

    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        return parse_hand(value)

    def to_python(self, value):
        if isinstance(value, Hand):
            return value

        if value is None:
            return value

        return parse_hand(value)

Σημειώστε ότι πάντα επιστρέφουμε ένα instance του Hand από αυτές τις μεθόδους. Αυτός είναι ο τύπος του Python object που θέλουμε να αποθηκεύσουμε στο attribute του model.

Όσον αφορά τη μέθοδο to_python(), αν κάτι πάει στραβά κατά τη μετατροπή της τιμής, θα πρέπει να κάνετε raise ένα ValidationError exception (όπως κάνουμε μέσα στην ``parse_hand ``).

Μετατρέποντας τα Python objects σε query values

Εφόσον χρησιμοποιούμε μια βάση δεδομέμων θα πρέπει, όπως είπαμε, η μετατροπή των δεδομένων να γίνεται μεταξύ Python και βάσης και αντίστροφα. Αν παρακάμψετε (override) τη μέθοδο to_python() θα χρειαστεί, επίσης, να παρακάμψετε τη μέθοδο get_prep_value() για να μετατρέψετε τα Python objects σε query values, δηλαδή τιμές που καταλαβαίνει η βάση δεδομένων. Η μέθοδος αυτή θα καλείται κάθε φορά που θα ερωτάται η βάση, μέσω κάποιου query, πχ MyModel.objects.filter(...) ή MyModel.objects.get(...) κλπ. Με άλλα λόγια, η μέθοδος αυτή προετοιμάζει την τιμή που δόθηκε στο query (η οποία είναι ένα Python object, πχ MyModel.objects.filter(hand=Hand('...26chars', '...26chars', '...26chars', '...26chars')) σε μια τιμή που καταλαβαίνει η βάση δεδομένων (στην προκειμένη, ένα string με όλα τα φύλλα της τράπουλας ανά παίκτη, πχ “Js6h3s5d6d…”).

Για παράδειγμα:

class HandField(models.Field):
    # ...

    def get_prep_value(self, value):
        return ''.join([''.join(l) for l in (value.north,
                value.east, value.south, value.west)])

Προειδοποίηση

Αν τα δικά σας πεδία χρησιμοποιούν του τύπους CHAR, VARCHAR ή TEXT της MySQL, θα πρέπει να σιγουρευτείτε ότι η μέθοδος get_prep_value() επιστρέφει πάντα μεταβλητές τύπου string. Όταν κάνετε ένα ερώτημα στη MySQL σχετικά με αυτά τα πεδία και η τιμή που δίνετε στο query είναι τύπου integer τότε η MySQL συμπεριφέρεται κάπως αναπάντεχα και τα αποτελέσματα από το query να μην είναι αυτά που περιμένετε. Αυτό το πρόβλημα δεν θα υπάρξει αν επιστρέφετε, πάντα, μια μεταβλητή τύπου string στη μέθοδο get_prep_value().

Μετατρέποντας τα query values σε database values

Κάποιοι τύποι δεδομένων (πχ ημερομηνίες) χρειάζεται να είναι σε κάποια συγκεκριμένη μορφή πριν χρησιμοποιηθούν με κάποια βάση δεδομένων. Η μέθοδος get_db_prep_value() είναι το μέρος όπου αυτές οι μετατροπές πρέπει να γίνουν. Η σύνδεση (της βάση δεδομένων) που χρησιμοποιείται με το query περνάει ως το όρισμα με το όνομα connection. Αυτό σας επιτρέπει να χρησιμοποιείτε διαφορετική λογική μετατροπής με διαφορετικούς τύπους βάσης δεδομένων, αν αυτό είναι απαραίτητο.

Για παράδειγμα, το Django χρησιμοποιεί την ακόλουθη μέθοδο για πεδία τύπου BinaryField:

def get_db_prep_value(self, value, connection, prepared=False):
    value = super().get_db_prep_value(value, connection, prepared)
    if value is not None:
        return connection.Database.Binary(value)
    return value

Σε περίπτωση που το δικό σας πεδίο χρειάζεται μια ξεχωριστή μετατροπή όταν αποθηκεύεται, δηλαδή όχι την ίδια μετατροπή που χρησιμοποιείται για συνηθισμένες παραμέτρους ενός query, μπορείτε να παρακάμψετε (override) τη μέθοδο get_db_prep_save().

Επεξεργασία τιμών πριν την αποθήκευση

Αν θέλετε να επεξεργαστείτε την τιμή ακριβώς πριν την αποθήκευση της (στη βάση δεδομένων), μπορείτε να χρησιμοποιήσετε τη μέθοδο pre_save(). Για παράδειγμα, η κλάση του Django DateTimeField χρησιμοποιεί αυτή τη μέθοδο για να θέσει το attribute στη σωστή τιμή, όταν το auto_now ή το auto_now_add είναι True.

Αν παρακάμψετε αυτή τη μέθοδο, θα πρέπει να επιστρέψετε στο τέλος την τιμή του attribute. Θα πρέπει, επίσης, να ενημερώσετε το αντίστοιχο attribute του μοντέλου σας αν κάνετε τυχόν αλλαγές στην τιμή ούτως ώστε τυχόν κώδικας που αναφέρεται στο μοντέλο θα βλέπει πάντα τη σωστή τιμή.

Προσδιορίζοντας το πεδίο της φόρμας για ένα πεδίο μοντέλου

Για να παραμετροποιήσετε το πεδίο της φόρμας (form field) που χρησιμοποιείται από την κλάση ModelForm, μπορείτε να παρακάμψετε (override) τη μέθοδο formfield().

Η κλάση του πεδίου της φόρμας μπορεί να οριστεί μέσω των arguments form_class και choices_form_class. Το τελευταίο χρησιμοποιείται σε περίπτωση που το πεδίο έχει ορίσει την παράμετρο choices, ενώ το πρώτο όχι. Αν αυτά τα arguments δεν δηλωθούν, τότε το πεδίο CharField ή TypedChoiceField θα χρησιμοποίηθεί.

Ολόκληρο το kwargs dictionary περνάει ως όρισμα απ” ευθείας στη μέθοδο __init__() του πεδίου της φόρμας. Συνήθως, το μόνο που πρέπει να κάνετε είναι να θέσετε ένα καλό kwargs ως προεπιλογή για το argument form_class (και ίσως για το choices_form_class) και μετά να δώσετε τον έλεγχο στην parent class. Αυτό ίσως απαιτήσει να γράψετε ένα δικό σας πεδίο φόρμας (και ίσως ένα widget φόρμας). Δείτε στο εγχειρίδιο για φόρμες, για πληροφορίες σχετικά με αυτό.

Συνεχίζοντας το παράδειγμα μας, μπορούμε να γράψουμε τη μέθοδο formfield() ως εξής:

class HandField(models.Field):
    # ...

    def formfield(self, **kwargs):
        # This is a fairly standard way to set up some defaults
        # while letting the caller override them.
        defaults = {'form_class': MyFormField}
        defaults.update(kwargs)
        return super().formfield(**defaults)

Αυτό προϋποθέτει ότι έχουμε κάνει import μια κλάση φόρμας MyFormField (η οποία έχει το δικό της προεπιλεγμένο widget). Το παρών άρθρο δεν καλύπτει τη συγγραφή δικών σας πεδίων για φόρμες.

Προσομοιώνοντας προεγκατεστημένους (built-in) τύπους πεδίων

Αν έχετε δημιουργήσει μια μέθοδο db_type(), δεν θα χρειαστεί να ανησυχείτε για τη μέθοδο get_internal_type() – δεν θα χρησιμοποιηθεί πολύ. Μερικές φορές, όμως, η μονάδα αποθήκευσης της βάσης δεδομένων σας είναι παρόμοια, σε επίπεδο τύπου, με κάποιο άλλο πεδίο, οπότε μπορείτε να χρησιμοποιήσετε τη λογική αυτού του άλλου πεδίου για να δημιουργήσετε τη σωστή τύπου στήλη.

Για παράδειγμα:

class HandField(models.Field):
    # ...

    def get_internal_type(self):
        return 'CharField'

Ανεξαρτήτως της βάσης δεδομένων που χρησιμοποιούμε, αυτό θα σημαίνει ότι η εντολή migrate και άλλες SQL εντολές, θα χρησιμοποιούν τη σωστή τύπου στήλη για την αποθήκευση ενός string.

Αν η μέθοδος get_internal_type() επιστρέφει ένα string το οποίο δεν είναι γνωστό στο Django για την βάση δεδομένων που χρησιμοποιείται – δηλαδή, δεν εμφανίζεται στο django.db.backends.<db_name>.base.DatabaseWrapper.data_types – το string θα εξακολουθήσει να χρησιμοποιείται από τον serializer, αλλά η προεπιλεγμένη μέθοδος db_type() θα επιστρέψει None. Δείτε το εγχειρίδιο της μεθόδου db_type() για περιπτώσεις που αυτό μπορεί να φανεί χρήσιμο. Είναι μια καλή ιδέα να θέσετε ένα όμορφα περιγραφικό string ως τύπο πεδίου για τον serializer αν πρόκειται να χρησιμοποιήσετε την έξοδο του serializer σε κάποιο άλλο μέρος, εκτός του Django.

Μετατρέποντας τα δεδομένα του πεδίου για το serialization

Για την παραμετροποίηση των τιμών ούτως ώστε να γίνουν serialized από τον serializer, μπορείτε να παρακάμψετε (override) τη μέθοδο value_to_string(). Η χρήση της value_from_object() είναι ο καλύτερος τρόπος να πάρετε την τιμή του πεδίου πριν το serialization. Για παράδειγμα, εφόσον το HandField χρησιμοποιεί strings για την αποθήκευση των δεδομένων του, μπορούμε να επαναχρησιμοποιήσουμε τον ήδη υπάρχον κώδικα που θα κάνει τη μετατροπή (get_prep_value()):

class HandField(models.Field):
    # ...

    def value_to_string(self, obj):
        value = self.value_from_object(obj)
        return self.get_prep_value(value)

Μερικές γενικές συμβουλές

Η συγγραφή ενός δικού σας πεδίου μπορεί να είναι μια περίπλοκη διαδικασία, ειδικά όταν πραγματοποιείτε περίπλοκες μετατροπές μεταξύ Python objects και μορφών της βάσης δεδομένων και του serialization. Παρακάτω παρουσιάζονται δύο συμβουλές για να ομαλοποιήσουν τα πράγματα:

  1. Κοιτάξτε στα ήδη υπάρχοντα πεδία του Django (μέσα στο αρχείο django/db/models/fields/__init__.py) για έμπνευση. Προσπαθήστε να βρείτε ένα πεδίο που να έχει ομοιότητες με αυτό που προσπαθήστε να επεκτείνετε, αντί να δημιουργήσετε ένα εντελώς νέο πεδίο από την αρχή.
  2. Put a __str__() method on the class you’re wrapping up as a field. There are a lot of places where the default behavior of the field code is to call str() on the value. (In our examples in this document, value would be a Hand instance, not a HandField). So if your __str__() method automatically converts to the string form of your Python object, you can save yourself a lot of work.

Γράφοντας μια subclass ενός πεδίου τύπου FileField

Επιπροσθέτως των παραπάνω μεθόδων, τα πεδια που ασχολούνται με αρχεία έχουν μερικές ακόμη απαιτήσεις, τις οποίες πρέπει να λάβετε υπόψιν σας. Η πλειονότητα των μηχανισμών που προσφέρει ένα πεδίο τύπου FileField, όπως ο έλεγχος της μονάδας αποθήκευσης της βάσης δεδομένων (database storage) καθώς και της ανάκτησης, μπορεί να μείνει ανέπαφος, αφήνοντας τις subclasses να χειριστούν την πρόκληση της υποστήριξης ενός συγκεκριμένου τύπου αρχείου.

Το Django προσφέρει μια κλάση File, που χρησιμοποιείται ως μεσάζων (proxy) στα περιεχόμενα και λειτουργίες του αρχείου. Αυτή η κλάση μπορεί να γίνει subclass προκειμένου να παραμετροποιηθεί η πρόσβαση στο αρχείο και το ποιες μέθοδοι θα είναι διαθέσιμες. Βρίσκεται στο django.db.models.fields.files και η προεπιλεγμένη συμπεριφορά του εξηγείται στο εγχειρίδιο για αρχεία.

Όταν μια subclass της κλάσης File δημιουργηθεί, η νέα subclass FileField θα πρέπει να ξέρει πως να χρησιμοποιήσει τη subclass της File. Για να γίνει αυτό, πρέπει να ορίσετε το ξεχωριστό attribute attr_class της FileField στην τιμή της νέας subclass της File.

Μερικές προτάσεις

Πέρα από τις παραπάνω λεπτομέρειες, υπάρχουν μερικές κατευθυντήριες γραμμές οι οποίες μπορούν να βελτιώσουν την αποδοτικότητα και την αναγνωσιμότητα του κώδικα του πεδίου σας.

  1. Ο πηγαίος κώδικας του πεδίου ImageField του Django (βρίσκεται στο αρχείο django/db/models/fields/files.py) είναι ένα καλό παράδειγμα του πως να κάνετε subclass την κλάση FileField για να υποστηρίξετε έναν συγκεκριμένο τύπο αρχείου, καθώς υιοθετεί όλες τις τεχνικές που αναφέρθηκαν παραπάνω.
  2. Όσο μπορείτε να κάνετε cache τα attributes του αρχείου. Εφόσον τα αρχεία μπορεί να αποθηκεύονται σε απομακρυσμένα συστήματα αποθήκευσης, η ανάκτηση τους μπορεί να κοστίσει επιπλέον άσκοπο χρόνο ή ακόμη και χρήματα. Όταν ένα αρχείο ανακτάται ούτως ώστε να δείτε κάποιες πληροφορίες σχετικά με τα περιεχόμενα του, κάντε cache (αποθηκεύστε τα στη λανθάνουσα μνήμη) όσα περισσότερα δεδομένα μπορείτε για να μειώσετε τον αριθμό μελλοντικών προσπελάσεων του αρχείου για την απόκτηση των ίδιων πληροφοριών.
Back to Top