Skip to content

ORM

Theory

ORM stands for Object - Relational Mapping (object relational mapping). A model is a Python object that abstracts database relationships. Django models provide an interface for reading and manipulating data from a database. Thanks to them, we do not have to build raw SQL queries in strings, we have objects representing entries in tables, we do not have to worry about the SQL dialect and the specificity of the database we use (ORM takes responsibility for this) and we do not have to worry about securing user data entered into queries (using ORM we are automatically protected against SQL-injection attacks).

Model class

Database relationships represent classes that inherit from Model. The columns are represented by objects with the suffix Field (e.g. CharField). We place the models in the models.py file.

from django.db.models import CharField, Model


class Genre(Model):
  name = CharField(max_length=128)

Migrations

During the launch of the project, we saw the following inscription:

migrations

Okay, red message ... it would be worth reading ...

To get rid of it, run the manage.py script with the migrate argument.

migrations

The above migrations were migrations of the part models built into Django. There were no tables in the database, so they had to be used. After their application, the database creates tables similar to the models built into Django.

Migrations describe changes taking place in models, so we started to need them in our project as well, because we added the model. The manage.py script when run with the makemigrations argument will create migration files for the models in our project.

migrations migrations migrations

The created migrations (files from the migrations folder) are stored in the code repository. We do not store the database in the repository, so if we want to refresh it we have to do python manage.py migrate even after checkout.

Shell

For "quick access" to the application from the developer side, Django provides the shell command: python manage.pyshell
This command is best used primarily for interacting with models. With it, you can, for example, test prepared queries:

shell

We now have access to a Python shell with the models loaded.

Model instances

Let's import the model:

from viewer.models import Genre

Let's check the model entries in the database:

Genre.objects.all()

The base is obviously empty. Let's add the first entries:

Genre.objects.create(name='Horror')

lub:

genre = Genre(name='Thriller') genre.save()

Now let's check the contents of the table again:

Genre.objects.all()

Let's take the horror genre entry from the database:

horror = Genre.objects.get(name='Horror')

Let's check if it's a horror movie:

horror.name

Each entry in the database also has a primary id:

horror.id

Model fields

Django's ORM has many classes that represent columns of database relationships. These fields can be found in the django.db.models module and are classes with the suffix Field.

from django.db.models import (
  DO_NOTHING, CharField, DateField, DateTimeField, ForeignKey, IntegerField,
  Model, TextField
)
class Movie(Model):
  title = CharField(max_length=128)
  genre = ForeignKey(Genre, on_delete=DO_NOTHING)
  rating = IntegerField()
  released = DateField()
  description = TextField()
  created = DateTimeField(auto_now_add=True)
  • CharField is a column type CHAR; as in SQL, we must specify its size
  • TextField is a column of the type VARCHAR or TEXT (ORM will use the appropriate type depending on the database used, we don't have to worry about it)
  • ForeignKey it represents a foreign key (the argument describes the table to which the key should refer); the convention is not followed because it is not a regular column - in the database it is a column of the primary key type of the second table and a foreign key constraint is created
  • IntegerField is a column type INT
  • DateField is a column of the type DATE (similarly with the column DATETIME or TIMESTAMP)

After deploying changes to the models, we should again properly handle the migration flow:

python manage.pymakemigrations

python manage.pymigrate

Administrator panel

Django generates an admin panel by default. We have already seen the url registered at urls.py :

from django.contrib import admin
from django.urls import path

from viewer.views import hello

urlpatterns = [
  path('admin/', admin.site.urls),
  path('hello/<s>', hello)
]

Let's visit the address http://localhost:8000/admin/. Let's create an administrator from the command line:

python manage.pycreatesuperuser

Now let's try to log in to the admin panel using the login details of the newly created user. Let's register our models in the admin panel:

from django.contrib import admin
from django.urls import path

from viewer.models import Genre, Movie
from viewer.views import hello

admin.site.register(Genre)
admin.site.register(Movie)

urlpatterns = [
  path('admin/', admin.site.urls),
  path('hello/<s>', hello)
]

After registration, the models should appear in the admin panel, from where we can view, add, modify and delete their instances. The changes made will be immediately entered into the database.

DUMP/LOAD

Data can be exported from the database:

python manage.pydumpdata viewer --output fixtures.json

After exporting, the data appears in the fixtures.json JSON file.

Data can be imported into the database:

python manage.pyloaddata fixtures.json

Of course, we should clean the database before loading the data. Otherwise we see the unique value violation error.

The exported data is called fixtures. Fixture is a good idea to use for automated tests or to manually test the application in our local development environments.

Queries

Thanks to the models, we can construct safe queries to the database. Here are some Python equivalents of the most popular SQL operators:

  • WHERE.filter
    • .filter returns multiple records
    • The usual comparison - by kwargsa:
      • Movie.objects.filter(rating=8) # films rated 8
      • horror = Genre.objects.get(name='Horror')
        Movie.objects.filter(genre=horror) # horrors
    • Comparison of nested fields - the so-called „lookup”:
      • Movie.objects.filter(genre__name='Horror') # horrors (second option)
    • Other comparisons - also „lookup
      • Movie.objects.filter(rating__gt=8) # rates greater than 8
      • Movie.objects.filter(title__contains='Godfather') # all parts of the Godfather (i.e. SQL LIKE; similarly `__icontains` is ILIKE)
        • NOTE: In SQLite, LIKE works like ILIKE in other databases, so we won't notice differences between __contains and __icontains!
      • Movie.objects.filter(title__in=['Se7en', 'Fight Club']) # „Se7en” i „Fight Club”
    • .filter can be "chained"
      • Movie.objects.filter(title__icontains=’godfather’) .filter(released__year__gt=1973)# parts of "The Godfather" published after 1973
  • WHERE NOT.exclude
    • It works exactly like .filter, but returns records not matching the lookup
  • Existence test:
    • Movie.objects.filter(title='Avatar').exists()# is there a movie called "Avatar"
  • COUNT.count
    • Movie.objects.filter(title__contains=’Godfather’).count()# how many parts of the "Godfather"
  • ORDER BY.order_by:
    • Movie.objects.filter(title__contains=’Godfather’)
      .order_by('released')# "The Godfather" parts sorted by oldest
    • Movie.objects.filter(title__contains=’Godfather’)
      .order_by('-released')# parts of "The Godfather" sorted by the newest

Data manipulation

We can manipulate data similarly:

  • CREATE
    • .create
      • Genre.objects.create(name='Documentary')# no access to the object before write
    • creation of an object + .save
      • genre =Genre(name='Comedy')
        genre.save()
  • UPDATE
    • .update
      • Updates the query results
      • Movie.objects.filter(released__year=2000).update(rating=5)# sets movie ratings from year 2000 to 5
    • changing the value of a field + .save
      • pulp_fiction = Movie.objects.get(title='Pulp Fiction')
        pulp_fiction.rating =7
        pulp_fiction.save()# here is the entry into the database
  • DELETE.delete
    • Movie.objects.filter(title__contains='Godfather').delete()# remove all parts of the "Godfather" from the base