246

Assume table has three columns: username, password and no_of_logins.

When user tries to login, it's checked for an entry with a query like

user = User.query.filter_by(username=form.username.data).first()

If password matches, he proceeds further. What I would like to do is count how many times the user logged in. Thus whenever he successfully logs in, I would like to increment the no_of_logins field and store it back to the user table. I'm not sure how to run update query with SqlAlchemy.

vvvvv
  • 25,404
  • 19
  • 49
  • 81
webminal.org
  • 44,948
  • 37
  • 94
  • 125

7 Answers7

533

There are several ways to UPDATE using SQLAlchemy:

  1. user.no_of_logins += 1
    session.commit()
    
  2. session.query(User).\
        filter(User.username == form.username.data).\
        update({'no_of_logins': User.no_of_logins + 1})
    session.commit()
    
  3. conn = engine.connect()
    stmt = User.update().\
        values(no_of_logins=User.no_of_logins + 1).\
        where(User.username == form.username.data)
    conn.execute(stmt)
    
  4. setattr(user, 'no_of_logins', user.no_of_logins + 1)
    session.commit()
    
ggorlen
  • 44,755
  • 7
  • 76
  • 106
Nima Soroush
  • 12,242
  • 4
  • 52
  • 53
  • 3
    I use ```setattr(query_result, key, value)``` for some updates in Flask SQLAlchemy followed by a commit. Any reason to discount this pattern? – Marc Feb 11 '16 at 20:57
  • 2
    @datamafia: You can use `setattr(query_result, key, value)`, which is exactly equivalent to writing `query_result.key = value` – Nima Soroush Feb 25 '16 at 09:42
  • Thx @NimaSoroush, that is what I do and yes equivalent. – Marc Feb 26 '16 at 00:47
  • 20
    Is it possible to explain the differences between these cases? Thanks! – Hatshepsut Mar 01 '17 at 20:10
  • 1
    @Hatshepsut One difference that I came across today between **4** and **1/2** is at: https://groups.google.com/forum/#!topic/sqlalchemy/wGUuAy27otM – Vikas Prasad Aug 28 '18 at 09:20
  • I asked a question here: https://stackoverflow.com/questions/52391072/creating-and-appending-to-a-list-in-sqlalchemy-database-table/52391339#52391339. I know that one of these options in the answer above must answer the question, but I've tried them all so I'm clearly not understanding it properly. Could someone point me to which option I should be looking at? – Slowat_Kela Sep 23 '18 at 19:32
  • 1
    No explicit `commit` is required in option 2 because `update` is a bulk update and doesn't honor ORM transactions. – Subhash Sep 06 '19 at 22:32
  • 8
    point number 2 is failing if you won't specify class object inside query(). I mean, the final query is ```session.query(User).filter(User.username == form.username.data).update({"no_of_logins": (User.no_of_logins +1)})``` – venkat Feb 27 '20 at 17:45
  • Again, doesn't option (1) introduce a race condition? better to do user.no_of_logins = User.no_of_logins + 1? – Jacob Lee Jul 24 '20 at 18:00
  • For option (3) I had to use `update(User)` rather than `User.update()`. – bfontaine Mar 02 '23 at 17:26
197
user.no_of_logins += 1
session.commit()
Denis
  • 7,127
  • 8
  • 37
  • 58
  • 25
    http://stackoverflow.com/a/2334917/125507 says this is a bad way to do it. Also what if the row doesn't exist yet? – endolith Aug 02 '14 at 18:13
  • 14
    As per endolith's link, `user.no_of_logins += 1` can create race conditions. Instead use `user.no_of_logins = user.no_of_logins + 1`. Translated into sql the latter correct way becomes: `SET no_of_logins = no_of_logins + 1`. – ChaimG Jul 14 '16 at 20:19
  • 13
    @ChaimG I guess you meant `user.no_of_logins = User.no_of_logins + 1`, or in other words use the instrumented attribute of the model to produce an SQL expression. As it is your comment displays 2 ways to produce the same race condition. – Ilja Everilä Dec 01 '17 at 07:09
  • Out of curiosity @IljaEverilä isn't `user.no_of_logins = User.no_of_logins + 1` *`==`* `user.no_of_logins += 1` if I'm not mistaken, I thought they're synonymous? – JayRizzo Sep 28 '18 at 02:32
  • 3
    They are not. The former sets the attribute to an SQL expression, the latter performs an in-place addition in Python and again introduces the race. – Ilja Everilä Sep 28 '18 at 04:28
  • 1
    @jayrizzo I'm not that familiar with SERIALIZABLE transaction isolation-level, but to my understanding it would allow performing the addition in Python using the in-place addition. If there is a race, one of the transactions will then succeed and the others will fail and must retry (with the new state of the DB). But I might've misunderstood SERIALIZABLE. – Ilja Everilä Sep 28 '18 at 04:42
  • 1
    Not sure myself about how SQLAlchemy would handle this but I found this https://stackoverflow.com/q/27351433/1896134 and led me to https://zodb.readthedocs.io/en/latest/transactions.html#retrying-transactions but, this depends on how you are using your SQLAlchemy https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html?highlight=serializable%20transaction%20sqlalchemy#setting-isolation-for-individual-sessions. If you are explicitly using this mode then you may get concurrency failures. but the first link may help with that. – JayRizzo Sep 28 '18 at 06:07
62

Examples to clarify the important issue in accepted answer's comments

I didn't understand it until I played around with it myself, so I figured there would be others who were confused as well. Say you are working on the user whose id == 6 and whose no_of_logins == 30 when you start.

# 1 (bad)
user.no_of_logins += 1
# result: UPDATE user SET no_of_logins = 31 WHERE user.id = 6

# 2 (bad)
user.no_of_logins = user.no_of_logins + 1
# result: UPDATE user SET no_of_logins = 31 WHERE user.id = 6

# 3 (bad)
setattr(user, 'no_of_logins', user.no_of_logins + 1)
# result: UPDATE user SET no_of_logins = 31 WHERE user.id = 6

# 4 (ok)
user.no_of_logins = User.no_of_logins + 1
# result: UPDATE user SET no_of_logins = no_of_logins + 1 WHERE user.id = 6

# 5 (ok)
setattr(user, 'no_of_logins', User.no_of_logins + 1)
# result: UPDATE user SET no_of_logins = no_of_logins + 1 WHERE user.id = 6

The point

By referencing the class instead of the instance, you can get SQLAlchemy to be smarter about incrementing, getting it to happen on the database side instead of the Python side. Doing it within the database is better since it's less vulnerable to data corruption (e.g. two clients attempt to increment at the same time with a net result of only one increment instead of two). I assume it's possible to do the incrementing in Python if you set locks or bump up the isolation level, but why bother if you don't have to?

A caveat

If you are going to increment twice via code that produces SQL like SET no_of_logins = no_of_logins + 1, then you will need to commit or at least flush in between increments, or else you will only get one increment in total:

# 6 (bad)
user.no_of_logins = User.no_of_logins + 1
user.no_of_logins = User.no_of_logins + 1
session.commit()
# result: UPDATE user SET no_of_logins = no_of_logins + 1 WHERE user.id = 6

# 7 (ok)
user.no_of_logins = User.no_of_logins + 1
session.flush()
# result: UPDATE user SET no_of_logins = no_of_logins + 1 WHERE user.id = 6
user.no_of_logins = User.no_of_logins + 1
session.commit()
# result: UPDATE user SET no_of_logins = no_of_logins + 1 WHERE user.id = 6
MarredCheese
  • 17,541
  • 8
  • 92
  • 91
  • Hello, thanks for your answer, instead of an int variable i am trying do update a string variable, how do i do that ? i am using an sqlite database and the variables i want to change are in current_user, through submitting a form – BHA Bilel May 20 '20 at 04:33
  • 1
    @danibilel Check out the documentation, which is pretty thorough. This part of the tutorial goes over updating string fields: https://docs.sqlalchemy.org/en/13/orm/tutorial.html#adding-and-updating-objects. Otherwise, I suggest you post a new question showing what you are stuck on specifically. – MarredCheese May 20 '20 at 17:00
  • Thanks for the link, after i spent the whole day searching, i know my problem is with db.session.commit(), it doesn't persist the changes in console as it should, i found similar questions but provided answers didn't work for me, in the actual web app it's even worse! the changes doesn't get saved at all, sorry for the long comment but i can't add a question because of the website's policy, any help would be appreciated. – BHA Bilel May 21 '20 at 04:38
7

I wrote telegram bot, and have some problem with update rows. Use this example, if you have Model

def update_state(chat_id, state):
    try:
        value = Users.query.filter(Users.chat_id == str(chat_id)).first()
        value.state = str(state)
        db.session.flush()
        db.session.commit()
        #db.session.close()
    except:
        print('Error in def update_state')

Why use db.session.flush()? That's why >>> SQLAlchemy: What's the difference between flush() and commit()?

MarredCheese
  • 17,541
  • 8
  • 92
  • 91
Andrew Lt
  • 183
  • 1
  • 4
  • 24
    The flush is entirely redundant just before commit. A commit always implicitly flushes. As much is said in the Q/A you linked to: "`flush()` is always called as part of a call to `commit()`" – Ilja Everilä Feb 23 '19 at 12:51
6

With the help of user=User.query.filter_by(username=form.username.data).first() statement you will get the specified user in user variable.

Now you can change the value of the new object variable like user.no_of_logins += 1 and save the changes with the session's commit method.

Nilesh
  • 20,521
  • 16
  • 92
  • 148
3

If you are using fastapi with sqlachemy you can update the row using SqlAlchecmy core like so (applicable of other python sqlalchmey code):

  • Update the complete record:
from sqlalchemy import update as sqlalchemy_update

 query = sqlalchemy_update(Evaluation).where(
            Evaluation.id == id).values(**evaluation.dict())
        await db.execute(query)
        await db.commit()

evaluation is the pydantic schema of the model Evaluation

  • PATCH (update a specific columns of the row): use an object the wrap the column you want to update with it is value example:
     query = sqlalchemy_update(DimensionsEvaluation).where(DimensionsEvaluation.evaluation_id == evluation_id).where(
                DimensionsEvaluation.dimension_id == id).values({
                    "selected": True
                })
            await db.execute(query)
DINA TAKLIT
  • 7,074
  • 10
  • 69
  • 74
0

just because this is the first result that comes up on google, I wanted to share a more scalable way to update a row with SQLAlchemy. It's based on the previous answers and I currently use it with an interface, allowing me to do table.update(**kwargs) through all my CRUD tables.

class User(Base):

    __tablename__ = 'users'

    user_id = Column(Integer, primary_key=True)
    first_name = Column(String)
    last_name = Column(String)

    def __init__(self, first_name: str, last_name: str, user_id: int = None):
        self.firtst_name = first_name
        self.last_name = last_name
        self.user_id = user_id

    def update(self, first_name: str, last_name: str, user_id: int = None, **kwargs):
        self.firtst_name = first_name
        self.last_name = last_name
        self.user_id = user_id
        
    def __repr__(self):
        return self.firtst_name

the update method is built into the ORM Table declaration itself. That way a table knows how it's meant to update itself. This may come in useful for future UPSERT operations.

with new_session() as post_session:
    post_data = request.form.to_dict(flat=False)
    client = Client.query.filter_by(id=post_data["id"]).first()
    client.update(**post_data)
    post_session.add(client)

the actual updating then becomes very simple. Generalizing from here is also easy.

David Mendes
  • 197
  • 2
  • 4