Atomic updates

Peewee allows you to perform atomic updates. Let’s suppose we need to updatesome 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 raceconditions 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 ouremployees 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 adenormalized column on the User model that stored the number of tweets auser had made, and we updated this value periodically. Here is how you mightwrite 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 SQLiteprior to 3.24.0 and MySQL, Peewee offers the replace(), whichallows 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.  
  5. # Insert or update the user. The "last_login" value will be updated
  6. # regardless of whether the user existed previously.
  7. user_id = (User
  8. .replace(username='the-user', last_login=datetime.now())
  9. .execute())
  10.  
  11. # This query is equivalent:
  12. user_id = (User
  13. .insert(username='the-user', last_login=datetime.now())
  14. .on_conflict_replace()
  15. .execute())

Note

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

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

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

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

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

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

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

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

Note

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

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

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

For more information, see Insert.on_conflict() andOnConflict.