2

Im a django newbie. Im making a crude hit counter as an assignment for a course in web programming at Uni. I made a class HitCount:

from django.db import models

# Create your models here.

class HitCount(models.Model):
    count = models.IntegerField()

And then I use this code in the views file:

def index(request):

    #try getting a hitcounter, if there is none, create one
    try:
        hc = HitCount.objects.get(pk=1)
    except: 
        hc = HitCount(count=0)
        hc.save()
        pass

    #get a queryset containing our counter
    hc = HitCount.objects.filter(pk=1)
    #increment its count and update it in db
    hc.update(count=F('count')+1)
    #ATM hc is a queryset, so hc.count will just return how many
    #counters are in the queryset (1). So we have to get the
    #actual counter object
    hc = HitCount.objects.get(pk=1)
    #and return its count
    return render_to_response('hitcount/index.html', {'count': hc.count})

This is my index.html file:

<p>{{count}}</p>

This seems to work just fine, but I wonder:

  • Is this a reasonable way of doing this? Should the code for incrementation really reside in the views file? Or should I move it into a method in the class?
  • Is this concurrency safe or do I need to use some kind of lock? Part of the assignment is making the counter concurrency safe. I use SQLite, which uses transactions, so I figured it should be all right, but I may be missing something.
fred
  • 1,812
  • 3
  • 37
  • 57
  • 1
    Transactions won't be used automatically. Although it does remove some of the niceties of an ORM, if you're using MySQL, this can be done in a single query: `INSERT INTO hitcount(id,count) VALUES(1,1) ON DUPLICATE KEY UPDATE count=count+1`. This avoids the need for transactions altogether. (You can use `MERGE INTO` for Oracle or SQL server, and a UDF in PostgreS) – Michael Mior Aug 29 '11 at 10:41
  • Cheers for your input. Would you mind expanding a little bit? E.g. how does one force the use of transactions? And how come using a manual query removes the need for transactions? Shouldnt there then be a risk that two queries are performed concurrently, leading to an error? – fred Aug 29 '11 at 15:07
  • 1
    See [here](https://docs.djangoproject.com/en/dev/topics/db/transactions/#django-s-default-transaction-behavior) for information on how Django handles transactions and how you can have Django automatically use transactions. As for my alternate suggestion, each update is performed in a single statement and treated as atomic by the database server, so no transactions. – Michael Mior Aug 29 '11 at 16:26

3 Answers3

3

Off topic, but you should be catching HitCount.DoesNotExist in your try/except, since you really only want to execute the code in the except if the HitCount object doesn't exist yet.

If it's possible, you might want to look at something like Redis (or another key/val store) to do your hit counter.

Redis provides a method called INCR that will automatically increment a value by 1. It's super fast and a great solution for a hit counter like this. All you need to do is make a key that is related to the page and you can increment that by +1.

It might also make more sense to use a middleware class to track page hits. Much easier than adding it to every view. If you need to display this count on every page, you can use a context processor (more info) to add the page's hit count into the context. There will be less code repetition this way.

Edit

I initially missed that this was for a Uni project, so this might be heavily over engineering for what you need. However, if you were to ever build a hit counter for a production environment, this is what I'd recommend. You can still use the middleware/context processors to do the hit counts/retrieval in a DRY manner.

Community
  • 1
  • 1
Alex Jillard
  • 2,792
  • 2
  • 19
  • 20
  • This is an excellent answer and great info! While Redis may be slight overkill in this particular case, I will definitely look into moving some of the code into a middleware class, it feels like a better design. Because you provided me with information that arguably answers both my questions, I will mark this as the accepted answer. – fred Aug 29 '11 at 15:11
2

Locking is possible in python using the following:

lock = Lock()
lock.acquire()
try:
    ... access shared resource
finally:
    lock.release() # release lock, no matter what

Keep in mind that method is not safe in a multi-server environment though.

You could also create a more extensible 'logging' solution that tracks each hit as a row in the db with associated info, and then be able to count/query even at a particular date range.

shawnwall
  • 4,549
  • 1
  • 27
  • 38
0

You could create a new database row for each hit and call HitCount.objects.count() to get the count.

Elver Loho
  • 1,266
  • 1
  • 9
  • 11
  • Thanks for your contribution, but arguably it is more clean and lightweight to simply increase the objects count value. – fred Aug 29 '11 at 15:03