About 10 years ago I wrote a post PostgreSQL for the Busy MySQL Developer, as part of switching from MySQL to Postgres for my personal and professional projects wherever I could.

Recently I had the chance to work with Python, Django, and Postgres as a long-time and busy Rails developer.

There were some things I thought were really nice. So am I switching?

The team I worked with was experienced with Django so I was curious to learn from them which libraries and tools are popular, and how how to write idiomatic code.

In this post I’ll briefly cover the database parts of Django using Postgres (of course!), highlight libraries and tools, and compare aspects to the Ruby on Rails framework. You’ll find a small Django repo towards the end as well.

Ruby versus Python

Ruby and Python are both general purpose programming languages. On the similarity side, they can both be used to write script style code, or organize code into classes using object oriented paradigms.

In local development, it felt like the execution of Python was perhaps faster than Ruby, however I’ve noticed that new apps are always fast to work with, given how little code is being loaded and executed.

Language runtime management

As a developer we typically need to run multiple versions of Ruby, Python, Node, and other runtimes, to support different codebases, and to avoid modifying our system installation.

In Ruby I use rbenv to manage multiple versions of Ruby, and to avoid using the version of Ruby that was installed by macOS, which is usually outdated compared with the version I want for a new app.

In Python, I used pyenv to accomplish the same thing, which seemed quite similar in use.

Both have concepts of a local and global version, and roughly similar commands to install and change versions.

Library management

In Ruby on Rails, Bundler has been the de facto standard forever, as a way to pull in Ruby library code and make sure it’s loaded and accessible in the Rails application.

In Python, the team selected the poetry dependency management tool.

Commands are similar to Bundler commands, for example poetry install is about the same as bundle install.

Dependencies can be expressed in a pyproject.toml file and poetry creates a lock file with specific library versions. TOML and YAML are similar.

Linting and formatting

In Ruby on Rails, although I personally resisted rule detection etc. for years, Rubocop has become the standard, even being built in to the most recent Rails version 8.

Rubocop has configurable rules that can automatically reformat code or lint code for issues.

Formatters like standardrb are commonly used as well.

For the Django app the team selected ruff, which performed formatting of code and linting for issues like missing imports.

I found ruff fast and easy to use and genuinely helpful.

For example, sometimes I’d fire up a Django shell, having skipped running ruff, only to realize there are issues it would have caught.

On this small codebase, ruff ran instantly, so it was a no-brainer to run regularly, or even include in my code editor.

Postgres adapter

In Rails and Django, SQLite is the default database, however I wanted to use Postgres.

In Ruby, we have the pg gem which connects the application to Postgres as a driver. This does work at a lower level than the application like sending TCP requests, mapping Postgres query result responses into Ruby data types, and much more.

In Python, we used the psycopg2 library and I found it pretty easy to use.

Besides being used by the framework ORM, I created a wrapper class using psycopg2 to use for sending SQL queries outside of models.

For example, we inspected Postgres system catalog views to capture certain data as part of the product features.

Migrations in Rails

Both Ruby on Rails and Django have the concept of Migrations, which are Ruby or Python code files that describe a database structure change, and have a version.

From the Ruby or Python code files, SQL DDL (or DML) statements are generated which are run against the configured database.

For example, to add a table in Rails typically a developer uses the create_table Ruby helper as opposed to writing a CREATE TABLE SQL statement.

Adding or dropping an index or modifying a column type are other types of DDL statements that typically are performed via migrations.

Migrations in Django

The Django approach has noteworthy differences and a slightly different workflow that I enjoyed more in some ways.

For example, changes are started in a models.py file, which contains all the application models (multiple models in a single file), and the database layer details about each model attribute.

This means that we specify database data types for columns, whether fields are unique, indexed, and more in the models file.

The interesting difference compared with Rails is that the next step in Django is to run makemigrations, which generates Python migration files.

This is different from Rails, where Rails developers would first generate a migration file to place changes into.

In Django the generated migration file can be inspected or simply applied using the migrate command. This command is nearly identical to the Rails equivalent command db:migrate.

For a new project where we were rapidly iterating on the models and their attributes, I preferred the way Django worked to how Rails works, or found it at least as productive.

Command line vibes

Here are some commands like running poetry install, or running manage.py commands like shell or makemigrations, to give you a flavor.

poetry install
python manage.py dbshell # psql in postgres
python manage.py shell   # Django shell
python manage.py makemigrations  # Generates Python migration files
python manage.py migrate # Runs migration files

Interactive console (REPL)

Both Django and Rails use interpreted languages, Python and Ruby respectively, that each support an interactive execution environment.

This environment is called a read, eval, print loop or REPL for short.

In Rails, the Ruby REPL “irb” is launched and Rails application code is loaded automatically when running the rails console command.

In Django the equivalent command is running shell, however application code needs to be imported before it can be used, using import statements.

Both frameworks also support opening a database client, by running dbconsole in Rails or dbshell in Django.

When Postgres is configured, these both open a psql session.

Projects and Apps

In Django projects and applications are separate concepts.

In my experimental project I made a “booksproject” project and a “books” app.

Check out the booksproject repo.

Postgres details

The books app models are Author, Publisher, and Books.

The tables for those models are contained in a custom schema booksapp, and Django is configured to access it.

The application connects to Postgres as the booksapp user and the dev database is called books_dev.

No migration safety concept

There’s no concept of what I’d call “safety” for migrations for either framework out of the box.

Operations like adding indexes in Postgres don’t use the concurrently keyword by default for example.

We can add safety using additional libraries like Strong Migrations in Ruby.

At a smaller scale of data and query volume, even unsafe operations will be fine. With that said, I think some visibility into blocking database operations, and how to perform them using safe alternatives is valuable.

Adding a constraint

In models add unique=True to a field definition to add a unique constraint (via a unique index). After running makemigrations a migration for a unique index will be created.

In Active Record we’d generate the migration file first, then fill in the create statement adding a unique index.

Django models

When querying a model like Book we’d use objects which returns a QuerySet object with one or more books.

The filter() method will generate a SQL query with a WHERE clause to filter down the rows or all rows can be accessed using all().

For example:

Model.objects.filter()
Model.objects.first()
Model.objects.all()

Statements in Python are whitespace sensitive so we’d indent the attributes in create() below by 4 spaces:

Thing.models.create(
    attr1=val,
    attr2=val
)

Previewing DDL

The generated SQL DDL isn’t displayed when running migrate by default.

Unlike Rails, Django provides a mechanism to preview it.

To do that run the sqlmigrate command instead of migrate.

For example, to print the 0001 migration DDL:

python manage.py sqlmigrate books 0001
BEGIN;
--
-- Create model Author
--
CREATE TABLE "books_author" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "first_name" varchar(200) NOT NULL, "last_name" varchar(200) NOT NULL);
COMMIT;

Note that Django uses an identity column for the primary key, and as of Rails 8 Active Record does not.

Resources

For the basics of an Author, Publisher, and Books models, or Postgres configuration including a custom schema and user, check out booksproject repo.

To collect random Django tips I’ve created a django-tips page. This page can be used in a similar way as my rails-tips and postgresql-tips pages, mostly as a reference for myself and possibly as a useful resource for others.

Wrap Up

Do you have any similarities and differences between Django and Rails to share? I’d love to hear from you.

😅 And no, I’m not “switching” from Rails and Ruby, but I did enjoy working with Python, Django, and Postgres!

Thanks for reading.