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.

MySQL supports upsert via the ON DUPLICATE KEY UPDATE clause. For example:

  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. preserve=[User.last_login], # Use the value we would have inserted.
  14. update={User.login_count: User.login_count + 1})
  15. .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.

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.

Note

The main difference between MySQL and Postgresql/SQLite is that Postgresql and SQLite require that you specify a conflict_target.

Here is a more advanced (if contrived) example using the EXCLUDED namespace. The EXCLUDED helper allows us to reference values in the conflicting data. For our example, we’ll assume a simple table mapping a unique key (string) to a value (integer):

  1. class KV(Model):
  2. key = CharField(unique=True)
  3. value = IntegerField()
  4. # Create one row.
  5. KV.create(key='k1', value=1)
  6. # Demonstrate usage of EXCLUDED.
  7. # Here we will attempt to insert a new value for a given key. If that
  8. # key already exists, then we will update its value with the *sum* of its
  9. # original value and the value we attempted to insert -- provided that
  10. # the new value is larger than the original value.
  11. query = (KV.insert(key='k1', value=10)
  12. .on_conflict(conflict_target=[KV.key],
  13. update={KV.value: KV.value + EXCLUDED.value},
  14. where=(EXCLUDED.value > KV.value)))
  15. # Executing the above query will result in the following data being
  16. # present in the "kv" table:
  17. # (key='k1', value=11)
  18. query.execute()
  19. # If we attempted to execute the query *again*, then nothing would be
  20. # updated, as the new value (10) is now less than the value in the
  21. # original row (11).

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