0

I am in a Ubuntu 22.04 Docker container with Python 3.10.

I use these apt packages:

mysql-client
libmysqlclient-dev

I use the following Python packages:

import mysqlclient==2.1.0
import sqlalchemy
# for the environment variables:
from dotenv import load_dotenv
# And to use sessions with flush, with the commit only at the end:
from sqlalchemy.orm import sessionmaker

The sessionmaker parameter autoflush is set to True.

I want to add a deletion and after that an insertion to a sqlalchemy.orm session so that I commit only when the two commands worked out well. The aim of this is to update a table that gets updated over the day. I do not want to delete anything before I am not sure that the insertion really works.

The part of the Python code that leads to this error (without the follow-up insertion command):

DELETE_QUERY = f"""
    DELETE FROM {MY_TABLE} 
    WHERE checkdate = DATE(NOW())
"""

def delete_execute(sess, conn, query):
    """Function for deleting and adding to sess values 
    from the DB
    :param connection: pymsql connection to DB
    :param query: SQL query containing DELETE keyword
    :return: count of rows deleted
    """
    try:
        cursor = conn.cursor()
        sess.add(cursor.execute(query))
        # # Not yet commit, only after insertion:
        # connection.commit()
        # # Updates the objects of the session:
        # sess.flush()
        # # Not needed here since autoflush is set to True
        return cursor.rowcount, sess

engine = create_engine(CONNECTION)
# Session = sessionmaker(engine)
Session = sessionmaker(autocommit=False, autoflush=True, bind=engine)

# Connect to DB
logging.info("Connecting to DB ...")

# # with statement closes the connection automatically,
# # see https://docs.sqlalchemy.org/en/14/dialects/mysql.html
# # But the class does not have the needed __enter__ attribute
# https://stackoverflow.com/questions/51427729/python-error-attributeerror-enter
# with engine.connect() as conn:
# # engine.connect() throws an error as well:
# conn = engine.connect() 
# # connection.cursor() AttributeError: 'Connection' object has no attribute 'cursor'
# https://stackoverflow.com/questions/38332787/pandas-to-sql-to-sqlite-returns-engine-object-has-no-attribute-cursor
conn = engine.raw_connection()

with Session() as sess:
    # The records only get deleted after commit
    # This only adds them to the session.
    deleted_records_count, sess = delete_execute(sess, conn, DELETE_QUERY)

I do not get the clue from other links on the same error:

The User model contains many Task models and the Task model contains many Subtask models.

...insert a new User into a DB using SQLAlchemy and Marshmallow.

I am new to sqlalchemy.orm and I fear that I have misunderstood something. I want to use a session for deletion and afterwards insertion of records, and I only want to commit in the end of the two commands. I use a cursor for the deletion. How can I embed the cursor - which is only available through the conn object from conn = engine.raw_connection() - so that the task is done only if the next insertion task works as well? I cannot just add it to the session:

sess.add(cursor.execute(query))

Which throws:

sqlalchemy.orm.exc.UnmappedInstanceError: Class 'builtins.int' is not mapped

The error in detail:

Traceback (most recent call last):
  File "/usr/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/MY_PROJECT/main.py", line 574, in <module>
    get_sql_and_save_as_csv_in_gcs(request)
  File "/MY_PROJECT/main.py", line 489, in get_sql_and_save_as_csv_in_gcs
    deleted_records_count, sess = delete_execute(sess, conn, DELETE_QUERY)
  File "/usr/local/lib/python3.9/dist-packages/dryable/__init__.py", line 34, in _decorated
    return function( * args, ** kwargs )
  File "/MY_PROJECT/main.py", line 204, in delete_execute
    sess.add(cursor.execute(query))
  File "/usr/local/lib/python3.9/dist-packages/sqlalchemy/orm/session.py", line 2601, in add
    util.raise_(
  File "/usr/local/lib/python3.9/dist-packages/sqlalchemy/util/compat.py", line 207, in raise_
    raise exception
sqlalchemy.orm.exc.UnmappedInstanceError: Class 'builtins.int' is not mapped

So how should I do it?

questionto42
  • 7,175
  • 4
  • 57
  • 90
  • 1
    I guess I don't use cursors. What is the purpose of a cursor in this case? I thought they were for special cases of selecting large result sets rather than for performing DELETE or INSERT. – Ian Wilson Jan 31 '22 at 05:06

1 Answers1

1
from sqlalchemy.sql import func

with Session() as session:
    # Model here is the orm Model, you could also do something similar
    # using sqlalchemy's core layer.
    cursor_result = session.query(Model).filter(Model.checkdate == func.now()).delete()
    deleted_rowcount = cursor_result.rowcount
    # This will insert each model at a time, maybe you want to do something
    # with better performance here.
    session.add_all(new_models)
    # commit delete and perform inserts
    session.commit()

It turns out using the orm delete above returns a cursor, you can read about it here: update-and-delete-with-arbitrary-where-clause

The core version, using a table object, should work with session.execute and you can read about it here: getting-affected-row-count-from-update-delete

Ian Wilson
  • 6,223
  • 1
  • 16
  • 24
  • These doc links are for SQLAlchemy 1.4, if you are using < 1.4 you might have to check those docs. – Ian Wilson Jan 31 '22 at 05:29
  • I see, you do not need the cursors at all (they are sort of legacy code here, you do not need to create the query strings with the passed arguments in the execute() statement anymore) and use the sqlalchemy models instead. For deleting and inserting, that should be ok, for the large select query (which I have not mentioned above), I will need batches, but I see I can put even that in a sqlalchemy model since it is just a slice using `LIMIT {offset}, {batch_size}`. And for a `SELECT`, I do not need to care about the late `commit` anyway. Thanks! – questionto42 Jan 31 '22 at 09:55
  • 1
    @questionto42 Yes, sqlalchemy provides `.limit()` and `.offset()` methods that can be chained to `select()` or `session.query(Model)` as well. Also see [yield-per](https://docs.sqlalchemy.org/en/14/orm/queryguide.html#yield-per) and [large_resultsets](https://docs.sqlalchemy.org/en/14/_modules/examples/performance/large_resultsets.html) – Ian Wilson Jan 31 '22 at 18:12