Atomic updates

Peewee allows you to perform atomic updates. Let’s suppose we need to update some counters. The naive approach would be to write something like this:

  1. >>> for stat in Stat.select().where(Stat.url == request.url):
  2. ... stat.counter += 1
  3. ... stat.save()

Do not do this! Not only is this slow, but it is also vulnerable to race conditions if multiple processes are updating the counter at the same time.

Instead, you can update the counters atomically using update():

  1. >>> query = Stat.update(counter=Stat.counter + 1).where(Stat.url == request.url)
  2. >>> query.execute()

You can make these update statements as complex as you like. Let’s give all our employees a bonus equal to their previous bonus plus 10% of their salary:

  1. >>> query = Employee.update(bonus=(Employee.bonus + (Employee.salary * .1)))
  2. >>> query.execute() # Give everyone a bonus!

We can even use a subquery to update the value of a column. Suppose we had a denormalized column on the User model that stored the number of tweets a user had made, and we updated this value periodically. Here is how you might write such a query:

  1. >>> subquery = Tweet.select(fn.COUNT(Tweet.id)).where(Tweet.user == User.id)
  2. >>> update = User.update(num_tweets=subquery)
  3. >>> update.execute()

Upsert

Peewee provides support for varying types of upsert functionality. With SQLite prior to 3.24.0 and MySQL, Peewee offers the replace(), which allows you to insert a record or, in the event of a constraint violation, replace the existing record.

Example of using replace() and on_conflict_replace():

  1. class User(Model):
  2. username = TextField(unique=True)
  3. last_login = DateTimeField(null=True)
  4. # Insert or update the user. The "last_login" value will be updated
  5. # regardless of whether the user existed previously.
  6. user_id = (User
  7. .replace(username='the-user', last_login=datetime.now())
  8. .execute())
  9. # This query is equivalent:
  10. user_id = (User
  11. .insert(username='the-user', last_login=datetime.now())
  12. .on_conflict_replace()
  13. .execute())

Note

In addition to replace, SQLite, MySQL and Postgresql provide an ignore action (see: on_conflict_ignore()) if you simply wish to insert and ignore any potential constraint violation.

Postgresql and SQLite (3.24.0 and newer) provide a different syntax that allows for more granular control over which constraint violation should trigger the conflict resolution, and what values should be updated or preserved.

Example of using on_conflict() to perform a Postgresql-style upsert (or SQLite 3.24+):

  1. class User(Model):
  2. username = TextField(unique=True)
  3. last_login = DateTimeField(null=True)
  4. login_count = IntegerField()
  5. # Insert a new user.
  6. User.create(username='huey', login_count=0)
  7. # Simulate the user logging in. The login count and timestamp will be
  8. # either created or updated correctly.
  9. now = datetime.now()
  10. rowid = (User
  11. .insert(username='huey', last_login=now, login_count=1)
  12. .on_conflict(
  13. conflict_target=(User.username,), # Which constraint?
  14. preserve=(User.last_login,), # Use the value we would have inserted.
  15. update={User.login_count: User.login_count + 1})
  16. .execute())

In the above example, we could safely invoke the upsert query as many times as we wanted. The login count will be incremented atomically, the last login column will be updated, and no duplicate rows will be created.

For more information, see Insert.on_conflict() and OnConflict.