<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Alex Tomkins - django</title>
    <link>https://www.alextomkins.com/tag/django/</link>
    <atom:link href="https://www.alextomkins.com/tag/django/feed.xml" rel="self" type="application/rss+xml" />
    <description>Posts tagged with django</description>
    <language>en</language>
    
    <item>
      <title>Avoiding cached datetimes with Django querysets</title>
      <link>https://www.alextomkins.com/2018/07/avoiding-cached-datetimes-with-django-querysets/</link>
      <guid>https://www.alextomkins.com/2018/07/avoiding-cached-datetimes-with-django-querysets/</guid>
      <pubDate>Sat, 21 Jul 2018 13:14:00 +0000</pubDate>
      <description><![CDATA[<p>In Django we often need to filter out objects from a queryset which shouldn't be visible to public
users, a typical example of this would be a news post in a blog. A staff user could edit a news
post to have a publish date in the future, allowing it to be automatically published by the site
without having to log back in and publish it.</p>
<p>A simple model for such a news post could look like:</p>
<pre><code>from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    published_at = models.DateTimeField(db_index=True)

    class Meta:
        ordering = ('-published_at',)

    def __str__(self):
        return self.title</code></pre>
<p>In this example, we're using a typical <code>ListView</code>, filtering out any posts which haven't yet been
published:</p>
<pre><code>from django.utils import timezone
from django.views.generic import ListView

from .models import Post

class PostListView(ListView):
    queryset = Post.objects.filter(published_at__lte=timezone.now())</code></pre>
<p>Note - we could use an <code>ArchiveIndexView</code> instead, which by default excludes objects from the
future. However for this example, we're sticking with <code>ListView</code> to show a simplified version of
the problem for other use cases.</p>
<p>When we first load the page, looking through the SQL queries generated for the request, we can see
the posts being filtered by their publish date:</p>
<pre><code>SELECT "news_post"."id",
       "news_post"."title",
       "news_post"."content",
       "news_post"."published_at"
  FROM "news_post"
 WHERE "news_post"."published_at" &lt;= '2018-07-21T11:02:12.998079+00:00'::timestamptz
 ORDER BY "news_post"."published_at" DESC</code></pre>
<p>At first this all seems okay, however at some point later on you'll realise that new posts aren't
being shown. Looking at the SQL queries generated for the following requests, we can see that the
timestamp doesn't change between requests:</p>
<pre><code>SELECT "news_post"."id",
       "news_post"."title",
       "news_post"."content",
       "news_post"."published_at"
  FROM "news_post"
 WHERE "news_post"."published_at" &lt;= '2018-07-21T11:02:12.998079+00:00'::timestamptz
 ORDER BY "news_post"."published_at" DESC</code></pre>
<p>Why? The queryset gets evaluated when the Django server starts, as the queryset is an attribute of
the generic view.</p>
<p>One solution for this is to move the queryset into the <code>get_queryset</code> method for the generic view:</p>
<pre><code>class PostListView(ListView):

    def get_queryset(self):
        return Post.objects.filter(published_at__lte=timezone.now())</code></pre>
<p>By using a method we're creating a new queryset for every request - using the current timestamp
when the request is generated. Problem solved!</p>
<p>However, since Django 1.9 there's a better way - let the database figure out the current time
stamp.</p>
<p>Instead of using <code>timezone.now()</code>, we can switch the view code to the <code>Now()</code> database function:</p>
<pre><code>from django.db.models.functions import Now
from django.views.generic import ListView

from .models import Post

class PostListView(ListView):
    queryset = Post.objects.filter(published_at__lte=Now())</code></pre>
<p>Looking at the SQL queries generated for the request, we can see that <code>STATEMENT_TIMESTAMP()</code> is
being used by Postgres to filter out any news posts</p>
<pre><code>SELECT "news_post"."id",
       "news_post"."title",
       "news_post"."content",
       "news_post"."published_at"
  FROM "news_post"
 WHERE "news_post"."published_at" &lt;= (STATEMENT_TIMESTAMP())
 ORDER BY "news_post"."published_at" DESC</code></pre>
<p>The same SQL query will be used for every request, which will now work as the current timestamp
gets evaluated by the database - and we don't need to create a <code>get_queryset</code> method for every
generic view!</p>
]]></description>
    </item>
    
    
    
    
    <item>
      <title>Fixing GDAL and GEOS for Django on macOS</title>
      <link>https://www.alextomkins.com/2017/08/fixing-gdal-geos-django-macos/</link>
      <guid>https://www.alextomkins.com/2017/08/fixing-gdal-geos-django-macos/</guid>
      <pubDate>Sun, 06 Aug 2017 15:32:00 +0000</pubDate>
      <description><![CDATA[<p>As a user of <a href="https://www.macports.org/">MacPorts</a> for all the additional packages needed when
working with Django on macOS, a recent upgrade to <a href="https://trac.osgeo.org/geos/">GEOS</a> managed to
break all my projects which used GeoDjango:</p>
<pre><code>$ ./manage.py help
Traceback (most recent call last):
  File "./manage.py", line 10, in &lt;module>
    execute_from_command_line(sys.argv)

  ...

  File "/Users/tomkins/.virtualenvs/greendale/lib/python3.5/site-packages/django/contrib/gis/geos/libgeos.py", line 147, in geos_version_info
    raise GEOSException('Could not parse version info string "%s"' % ver)
django.contrib.gis.geos.error.GEOSException: Could not parse version info string "3.6.2-CAPI-1.10.2 4d2925d6"

$ port installed geos
The following ports are currently installed:
  geos @3.6.2_0 (active)

</code></pre>
<p>Highly annoying!</p>
<p>This will be fixed in <a href="https://github.com/django/django/pull/8817">PR #8817</a> for Django master,
which will be released in Django 2.0 later this year, and
<a href="https://github.com/django/django/pull/8841">PR #8841</a> for the upcoming Django 1.11.5 release.
However the fix won't be backported to older versions of Django, such as the Django 1.8 LTS branch.
So to continue using GeoDjango on macOS, we need to use an older working version of GEOS.</p>
<h2>KyngChaos packages</h2>
<p>Fortunately <a href="http://www.kyngchaos.com/">KyngChaos</a> has a variety of
<a href="http://www.kyngchaos.com/software/frameworks">Unix Compatibility Frameworks</a> available for
download, including GDAL and GEOS. Fortunately it's an older version of GEOS (3.6.1) which will
still work with older versions of Django. Also the older version of GDAL (1.11) works with Django
1.8, as newer versions of GDAL also cause problems with Django 1.8.</p>
<p>Download and install:</p>
<ul>
<li>GDAL 1.11 Complete</li>
<li>GDAL 2.1 Complete</li>
</ul>
<p>Although we won't be using GDAL 2.1 in this example, you can easily switch to it if you're only
running Django 1.11.</p>
<p>Add the following to your <code>.bash_profile</code>:</p>
<pre><code>export GDAL_LIBRARY_PATH="/Library/Frameworks/GDAL.framework/Versions/1.11/GDAL"
export GEOS_LIBRARY_PATH="/Library/Frameworks/GEOS.framework/Versions/3/GEOS"</code></pre>
<p>Then add the following to your Django settings file:</p>
<pre><code># GeoDjango fixes
GDAL_LIBRARY_PATH = os.environ.get('GDAL_LIBRARY_PATH')
GEOS_LIBRARY_PATH = os.environ.get('GEOS_LIBRARY_PATH')</code></pre>
<p>On the server you're deploying these environment variables won't be set, so the setting will
default to <code>None</code> - in which case Django will automatically find the installed versions of GDAL and
GEOS.</p>
<p>Now you should have a fully functioning GeoDjango project on macOS!</p>
]]></description>
    </item>
    
    
    
    <item>
      <title>The cost of Dirty Fields</title>
      <link>https://www.alextomkins.com/2016/12/the-cost-of-dirtyfields/</link>
      <guid>https://www.alextomkins.com/2016/12/the-cost-of-dirtyfields/</guid>
      <pubDate>Sun, 04 Dec 2016 16:13:00 +0000</pubDate>
      <description><![CDATA[<p>After installing <a href="https://github.com/romgar/django-dirtyfields">Django Dirty Fields</a> on projects a
few months ago and seeing a dramatic reduction in the number of writes to our main Postgres
database - everything seemed fine. However on a brand new project, something wasn't quite right
performance wise:</p>
<pre><code>$ siege --concurrent=1 --reps=10 "http://127.0.0.1:8000/map/?lat=51.4995&amp;lng=0.1248"
...

Transactions:                     10 hits
Availability:                 100.00 %
Elapsed time:                  11.85 secs
Data transferred:               2.27 MB
Response time:                  0.88 secs
Transaction rate:               0.84 trans/sec
Throughput:                     0.19 MB/sec
Concurrency:                    0.75
Successful transactions:          10
Failed transactions:               0
Longest transaction:            0.96
Shortest transaction:           0.83
</code></pre>
<p>Painfully slow! Although it wasn't the most optimised code possible, an average of 880ms per
request wasn't acceptable.</p>
<h1>Investigating the cause</h1>
<p>As the view for this request generates a JSON response, using
<a href="https://github.com/jazzband/django-debug-toolbar">Django Debug Toolbar</a> wasn't a viable option -
as all the debugging output gets attached to HTML responses only. So instead I decided to run
through the code with <code>shell_plus</code> from
<a href="https://github.com/django-extensions/django-extensions">Django Extensions</a> and
<a href="https://ipython.org/">IPython</a>.</p>
<p>After going through parts of the code, one of the querysets seemed slower than expected:</p>
<pre><code>>>> %timeit venue_list = list(Venue.objects.all()[:100])
10 loops, best of 3: 101 ms per loop</code></pre>
<p>Over 100ms to go through a fairly small queryset? This is far too slow. Just to see if it's a
database problem, we'll change it to use <code>values_list</code> instead, which just returns a list of
tuples:</p>
<pre><code>>>> %timeit venue_list = Venue.objects.values_list('id', 'name', 'location')
10000 loops, best of 3: 80.3 µs per loop</code></pre>
<p>And testing another model from another app as a quick sanity check to ensure there's no problems
with other models:</p>
<pre><code>>>> %timeit permission_list = list(Permission.objects.all())
100 loops, best of 3: 2.29 ms per loop</code></pre>
<p>So something is obviously wrong with the queryset/model.</p>
<p>After seeing that this model had <code>DirtyFieldsMixin</code>, which was one obvious difference between this
model and all the others, the next test was to remove it and see if that made any difference:</p>
<pre><code>>>> %timeit venue_list = list(Venue.objects.all()[:100])
100 loops, best of 3: 8.04 ms per loop</code></pre>
<p>From 101ms to 8ms.</p>
<p>After removing all instances of <code>DirtyFieldsMixin</code>, another performance test showed an improvement:</p>
<pre><code>$ siege --concurrent=1 --reps=10 "http://127.0.0.1:8000/map/?lat=51.4995&amp;lng=0.1248"
...

Transactions:                     10 hits
Availability:                 100.00 %
Elapsed time:                   7.98 secs
Data transferred:               2.27 MB
Response time:                  0.40 secs
Transaction rate:               1.25 trans/sec
Throughput:                     0.28 MB/sec
Concurrency:                    0.50
Successful transactions:          10
Failed transactions:               0
Longest transaction:            0.44
Shortest transaction:           0.36
</code></pre>
<p>From 880ms to 400ms - a big difference.</p>
<h1>Testing django-model-utils</h1>
<p>As Django Dirty Fields wasn't great for performance, an alternative which seems to offer similar
functionality is the tracker field from
<a href="https://github.com/carljm/django-model-utils">django-model-utils</a>.</p>
<p>Let's test by adding a <code>FieldTracker</code> field to a model:</p>
<pre><code>>>> %timeit venue_list = list(Venue.objects.all()[:100])
10 loops, best of 3: 21.5 ms per loop</code></pre>
<p>Much faster than Django Dirty Fields!</p>
<p>Some of the code when saving/updating objects needs updating for django-model-utils, as it doesn't
have the same convenience methods:</p>
<pre><code>if venue.id is None:
    venue.save()
else:
    changed_fields = venue.tracker.changed().keys()
    if changed_fields:
        venue.save(update_fields=changed_fields)</code></pre>
<p>One subtle difference between the two packages is that you'll need to ensure the data you enter is
the same type. It's possible to give an <code>IntegerField</code> a string value, and Django will still save
it.</p>
<p>With Django Dirty Fields:</p>
<pre><code>>>> venue = Venue.objects.get(id=14132)
>>> venue.grid_ref_x
442478
>>> venue.grid_ref_x = '442478'
>>> venue.is_dirty()
False
>>> venue.save()
>>> venue.grid_ref_x
'442478'
>>> venue.grid_ref_x = '123'
>>> venue.is_dirty()
True
>>> venue.save()
>>> venue.grid_ref_x
'123'
>>> venue.refresh_from_db()
>>> venue.grid_ref_x
123</code></pre>
<p>With django-model-utils:</p>
<pre><code>>>> venue = Venue.objects.get(id=14132)
>>> venue.grid_ref_x
442478
>>> venue.grid_ref_x = '442478'
>>> venue.tracker.changed()
{'grid_ref_x': 442478}</code></pre>
<p>Which would result in a lot of additional saves for data, even though the saved data will end up
being the same.</p>
<h1>Using proxy models</h1>
<p>Given that none of the code in the Django views used the dirty fields methods, and won't use the
tracker field either - this seems like an ideal case for proxy models in Django. Instead of adding
the tracker to the <code>Venue</code> model, we'll create a proxy model instead:</p>
<pre><code>class VenueTracker(Venue):
    tracker = FieldTracker()

    class Meta:
        proxy = True</code></pre>
<p>Now we've got two versions of the same model:</p>
<pre><code>>>> %timeit venue_list = list(Venue.objects.all()[:100])
100 loops, best of 3: 8.85 ms per loop
>>> %timeit venue_list = list(VenueTracker.objects.all()[:100])
10 loops, best of 3: 21.2 ms per loop</code></pre>
<p>So by default we'll use the standard <code>Venue</code> model in views for speed, however if we need a version
with tracking for our update scripts, we simply change the imports to point to the model with a
tracker:</p>
<pre><code>from .models import VenueTracker as Venue</code></pre>
<p>Now we can have fast views as normal, but with the option to switch to the tracked version if
needed.</p>
]]></description>
    </item>
    
    
    
    <item>
      <title>Modern Django with Ubuntu Trusty</title>
      <link>https://www.alextomkins.com/2016/10/modern-django-with-ubuntu-trusty/</link>
      <guid>https://www.alextomkins.com/2016/10/modern-django-with-ubuntu-trusty/</guid>
      <pubDate>Sun, 02 Oct 2016 22:57:00 +0000</pubDate>
      <description><![CDATA[<p>At this point Ubuntu 14.04 Trusty Tahr is nearly 2.5 years old, with another 2.5 years support left
until it reaches end of life for support. However for those of us still working with Trusty, it's
often desirable to try and get a few backported modern packages to make development and hosting of
Django apps a bit easier to work with.</p>
<p>These days you could use containers to deploy bleeding edge applications with all the new stuff
bundled inside, maybe with either <a href="https://www.docker.com/">Docker</a> or
<a href="https://coreos.com/rkt/">rkt</a>. However in this post we'll be going through a couple of available
APT repositories to help modernise a slightly older distribution.</p>
<h1>Python</h1>
<p>As of October 2016, Python 3.5 is the latest stable version of Python. It's supported in Django 1.8
to 1.10, and for anyone wanting to develop an app for the long term - going for Python 2.7 at this
point in time is a dead end. Ubuntu Trusty only has Python 2.7 and 3.4, although 3.4 is well
supported - we want to try and go for the latest possible Python version.</p>
<h2>Deadsnakes</h2>
<p>The <a href="https://launchpad.net/~fkrull/+archive/ubuntu/deadsnakes">Old and New Python Versions</a>
repository (or deadsnakes) provides multiple Python versions which aren't included in a particular
version of Ubuntu. Consider the support of this repository carefully, as it isn't an official repo
(stick with Python 3.4 if this bothers you).</p>
<p>Installation is quick and easy:</p>
<pre><code>$ sudo add-apt-repository ppa:fkrull/deadsnakes
$ sudo apt-get update
$ sudo apt-get install python3.5</code></pre>
<p>As deadsnakes provides even more versions - we could also install Python 3.3 as well, which could
help in testing code under multiple Python versions with <a href="https://tox.wiki/">tox</a>.</p>
<h1>PostgreSQL</h1>
<p>Ubuntu Trusty comes with PostgreSQL 9.3, which is missing JSONB support. So if we want the
JSONField which is new since Django 1.9 - we'll need a more modern version of Postgres. As of
October 2016 the latest release of Postgres is 9.6 - which is what we want to aim for.</p>
<h2>PostgreSQL Apt Repository</h2>
<p>Fortunately postgresql.org provides official packages of all the supported Postgres versions for
quite a few supported distributions. As this is provided by the official Postgres site, packages
are updated with every new release - so support is good.</p>
<p>To install:</p>
<pre><code>$ sudo add-apt-repository "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main"
$ curl -sL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get install postgresql-9.6</code></pre>
<p>Now we can use JSONField thanks to the updated Postgres.</p>
<h1>Conclusion</h1>
<p>Just because you're stuck on an older release of Ubuntu doesn't mean you're stuck with all the old
tools - go upgrade!</p>
]]></description>
    </item>
    
    
    
    <item>
      <title>Easier PostgreSQL Extension Installations</title>
      <link>https://www.alextomkins.com/2016/09/easier-postgresql-extension-installations/</link>
      <guid>https://www.alextomkins.com/2016/09/easier-postgresql-extension-installations/</guid>
      <pubDate>Sat, 24 Sep 2016 20:32:00 +0000</pubDate>
      <description><![CDATA[<p>Postgres has a number of extensions which require superuser access for a user to install them,
<a href="http://postgis.net/">PostGIS</a> being one of them. If you're wanting to install a
<a href="https://docs.djangoproject.com/en/1.8/ref/contrib/gis/">GeoDjango</a> app, you'll probably encounter
an error such as:</p>
<pre><code>$ ./manage.py migrate
Traceback (most recent call last):
  ..
django.db.utils.ProgrammingError: permission denied to create extension "postgis"
HINT:  Must be superuser to create this extension.
</code></pre>
<p>For security reasons you don't want to give the user superuser access, and for convenience you
don't want to go and manually go into each database just to add PostGIS support.</p>
<h1>PostgreSQL Extension Whitelisting</h1>
<p>The <a href="https://github.com/dimitri/pgextwlist">pgextwlist</a> extension can allow regular users to use
the <code>CREATE EXTENSION</code> command, allowing them to install the extension without getting the
permission denied error.</p>
<p>To install on Ubuntu 16.04 (Xenial Xerus):</p>
<pre><code>$ sudo apt-get install postgresql-9.5-pgextwlist</code></pre>
<p>To install on Debian 8 (Jessie):</p>
<pre><code>$ sudo apt-get install postgresql-9.4-pgextwlist</code></pre>
<p>After installing, edit the relevant <code>postgresql.conf</code> file for your Postgres instance and add the
following lines:</p>
<pre><code># Allow PostGIS to be loaded easily
local_preload_libraries = 'pgextwlist'
extwlist.extensions = 'postgis'</code></pre>
<p>Then run:</p>
<pre><code>$ sudo service postgresql restart</code></pre>
<p>Now you should be able to use a GeoDjango app/database without any errors:</p>
<pre><code>$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  ..
</code></pre>
]]></description>
    </item>
    
    
    
    
    
    
    <item>
      <title>Speed up Django static files</title>
      <link>https://www.alextomkins.com/2016/08/speed-up-django-static-files/</link>
      <guid>https://www.alextomkins.com/2016/08/speed-up-django-static-files/</guid>
      <pubDate>Sun, 28 Aug 2016 21:16:00 +0000</pubDate>
      <description><![CDATA[<p>For a fairly easy performance win, and for something which makes dealing with old cached CSS
something a thing of the past - enable
<a href="https://docs.djangoproject.com/en/1.8/ref/contrib/staticfiles/#manifeststaticfilesstorage">ManifestStaticFilesStorage</a>.</p>
<p>First of all you'll need to edit <code>settings.py</code>:</p>
<pre><code># Use manifest static storage
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'</code></pre>
<p>Then you'll need to edit any templates which aren't using the static template tag. Instead of
using:</p>
<pre><code>&lt;img src="{{ STATIC_URL }}images/hello.jpg" %}" alt="Hello"></code></pre>
<p>You'll need to use:</p>
<pre><code>{% load static from staticfiles %}
&lt;img src="{% static 'images/hello.jpg' %}" alt="Hello"></code></pre>
<p>Now when you run <code>django-admin collectstatic</code>, Django will include the MD5 hash of the file as part
of the file name. Now when you use the <code>{% static %}</code> tag you'll see the file name with the hash.</p>
<p>If you're running nginx, update your site configuration so that browsers will cache static files
for as long as possible:</p>
<pre><code>location /static/ {
    expires max;
}</code></pre>
<p>Then reload nginx.</p>
<p>Now you should be up and running with a good performance boost, and you won't have to ask users to
refresh a page to get updated static files.</p>
]]></description>
    </item>
    
    
    
    <item>
      <title>Be careful with Django&#39;s .create_or_update()!</title>
      <link>https://www.alextomkins.com/2016/08/be-careful-with-djangos-create-or-update/</link>
      <guid>https://www.alextomkins.com/2016/08/be-careful-with-djangos-create-or-update/</guid>
      <pubDate>Sun, 21 Aug 2016 18:44:00 +0000</pubDate>
      <description><![CDATA[<p>Although using <code>Model.objects.create_or_update()</code> with a Django model is extremely convenient,
sometimes you might want to consider using it carefully with certain usage patterns.</p>
<h1>Excessive Postgres WAL files</h1>
<p>To allow point-in-time recovery (PITR), I usually setup <a href="http://www.pgbarman.org/">Barman</a>. Set it
up on a remote server, get your Postgres instance to rsync files over to the Barman server - and
you've got a set of backups which should allow you to recover your database from an earlier point
in time.</p>
<p>For every UPDATE/INSERT, Postgres will write data to the WAL (Write-Ahead Log). This isn't a
problem if you're not doing anything else with the finished WAL files, although you'll have
increased disk I/O - it's probably just a minor increase you won't notice too much.</p>
<p>However if you're keeping the WAL and archiving it for backups, every update to a table row will be
backed up:</p>
<pre><code>$ barman list-backup all
golestandt 20160613T063007 - Mon Jun 13 06:36:06 2016 - Size: 4.5 GiB - WAL Size: 12.7 GiB
</code></pre>
<p>Ouch.</p>
<h1>Finding the problem databases</h1>
<p>Assuming you've got statistics enabled, use <code>psql</code> to show the statistics for each database:</p>
<pre><code>postgres=# SELECT datname,tup_updated,tup_inserted,tup_deleted FROM pg_stat_database ORDER BY tup_updated DESC;
                datname                | tup_updated | tup_inserted | tup_deleted
---------------------------------------+-------------+--------------+-------------
 site1                                 |     1138475 |       191605 |      136569
 site2                                 |      153224 |        46650 |       12385</code></pre>
<p>Now we've got a list of databases which are excessively updating and inserting new rows.</p>
<h1>How did we get here?</h1>
<p>By being too lazy with <code>.create_or_update()</code> in applications doing regular syncs.</p>
<p>If you've got Django code syncing with a third party service, it's easy to write code similar to:</p>
<pre><code>for tweet in tweet_list:
    Tweet.objects.create_or_update(tweet_id=tweet['id'], defaults={
        'user': tweet['user']['screen_name'],
        'text': tweet['text'],
        'retweet_count': tweet['retweet_count'],
    })</code></pre>
<p>If you're only updating a few objects occasionally - this is fine and does the job. However if
you're updating hundreds of objects every hour - you could end up with hundreds of thousands of
rows being updated on a weekly basis, even though most of the data is likely to stay the same.</p>
<p>Every field for the object gets updated every time the object gets saved. If you've got a model
with more fields, this will add up very quickly to additional WAL data.</p>
<h1>Avoiding updates</h1>
<p>If only some of the synced data is actually being updated - just update the fields which will
receive any updates:</p>
<pre><code>for tweet in tweet_list:
    obj, created = Tweet.objects.get_or_create(tweet_id=tweet['id'], defaults={
        'user': tweet['user']['screen_name'],
        'text': tweet['text'],
        'retweet_count': tweet['retweet_count'],
    })

    if not created:
        # Update counts, but try to avoid excessive updates
        update_fields = []

        if obj.retweet_count != tweet['retweet_count']:
            obj.retweet_count = tweet['retweet_count']
            update_fields.append('retweet_count')

        if update_fields:
            obj.save(update_fields=update_fields)</code></pre>
<p>In this example we're assuming tweet IDs or usernames won't ever change. This is probably a safe
assumption for IDs, and slightly less so for usernames.</p>
<p>By switching to <code>.get_or_create()</code>, and using <code>.save(update_fields=list)</code> - we've significantly
reduced the number of updates. If the object already exists and none of the fields have been
changed - <code>update_fields</code> will be an empty list and we don't even bother with <code>.save()</code>.</p>
<p>The only downside using this method is that it can be tedious having to check every single field
for updates. We could probably improve this with a more generic solution - but at this point it's
easier to use a third party package which solves the problem.</p>
<h1>Django Dirty Fields</h1>
<p><a href="https://github.com/romgar/django-dirtyfields">Django Dirty Fields</a> is an easy alternative to
writing your own code to check all of the fields of an object. Just add a <code>DirtyFieldsMixin</code> to
your model and you can simplify your code:</p>
<pre><code>for tweet in tweet_list:
    try:
        obj = Tweet.objects.get(tweet_id=tweet['id'])
    except Tweet.DoesNotExist:
        obj = Tweet(tweet_id=tweet['id'])

    obj.user = tweet['user']['screen_name']
    obj.text = tweet['text']
    obj.retweet_count = tweet['retweet_count']

    # Only save if needed
    if not obj.id:
        obj.save()
    elif obj.is_dirty():
        obj.save_dirty_fields()</code></pre>
<p>Considerably easier to deal with!</p>
<p>The one huge advantage of this approach is that you can add as many fields as you want, and Django
Dirty Fields will do the job of figuring out which fields to update for you. As before if none of
the fields have been updated - the object won't be saved.</p>
<h1>The Results</h1>
<p>After trying to fix several projects:</p>
<pre><code>$ barman list-backup all
golestandt 20160704T063009 - Mon Jul  4 06:33:29 2016 - Size: 4.6 GiB - WAL Size: 822.0 MiB
</code></pre>
<p>There's still a few old projects which haven't been optimised - however we've now managed to reduce
the size of the weekly WAL to be smaller than the weekly snapshot.</p>
]]></description>
    </item>
    
    
    
    
    
    
    
    
    
    
  </channel>
</rss>
