SQLAlchemy: crear frente a reutilizar una sesión

98

Solo una pregunta rápida: SQLAlchemy habla de llamar sessionmaker()una vez, pero llamar a la Session()clase resultante cada vez que necesite hablar con su base de datos. Para mí eso significa que el segundo que haría mi primero session.add(x)o algo similar, primero haría

from project import Session
session = Session()

Lo que hice hasta ahora fue hacer la llamada session = Session()en mi modelo una vez y luego importar siempre la misma sesión en cualquier lugar de mi aplicación. Dado que se trata de una aplicación web, esto normalmente significaría lo mismo (a medida que se ejecuta una vista).

Pero, ¿dónde está la diferencia? ¿Cuál es la desventaja de usar una sesión todo el tiempo frente a usarla para las cosas de mi base de datos hasta que termine mi función y luego crear una nueva la próxima vez que quiera hablar con mi base de datos?

Entiendo que si utilizo varios hilos, cada uno debería tener su propia sesión. Pero al usar scoped_session(), ya me aseguro de que ese problema no exista, ¿verdad?

Aclare si alguna de mis suposiciones es incorrecta.

javex
fuente

Respuestas:

223

sessionmaker()es una fábrica, está ahí para fomentar la colocación de opciones de configuración para crear nuevos Sessionobjetos en un solo lugar. Es opcional, ya que puede llamar con la misma facilidad en Session(bind=engine, expire_on_commit=False)cualquier momento que necesite un nuevo Session, excepto que es detallado y redundante, y quería detener la proliferación de "ayudantes" a pequeña escala que abordan el tema de esta redundancia en algún nuevo y forma más confusa.

Entonces sessionmaker()es solo una herramienta para ayudarlo a crear Sessionobjetos cuando los necesite.

Parte siguiente. Creo que la pregunta es, ¿cuál es la diferencia entre hacer una nueva Session()en varios puntos versus simplemente usar una hasta el final? La respuesta, no mucha. Sessiones un contenedor para todos los objetos que coloca en él, y luego también realiza un seguimiento de una transacción abierta. En el momento en que llama a rollback()o commit(), la transacción ha terminado y Sessionno tiene conexión con la base de datos hasta que se le pide que emita SQL nuevamente. Los enlaces que contiene a sus objetos mapeados son referencias débiles, siempre que los objetos estén limpios de cambios pendientes, por lo que incluso en ese sentido, Sessionse vaciará a un estado completamente nuevo cuando su aplicación pierda todas las referencias a objetos mapeados. Si lo dejas con su valor predeterminado"expire_on_commit", todos los objetos caducan después de una confirmación. Si eso se Sessionqueda durante cinco o veinte minutos, y todo tipo de cosas han cambiado en la base de datos la próxima vez que la use, se cargará todo el estado nuevo la próxima vez que acceda a esos objetos a pesar de que han estado almacenados en la memoria. durante veinte minutos.

En las aplicaciones web, solemos decir, oye, ¿por qué no haces una nueva Sessionen cada solicitud, en lugar de usar la misma una y otra vez? Esta práctica asegura que la nueva solicitud comience "limpia". Si algunos objetos de la solicitud anterior aún no se han recolectado como basura, y si tal vez los desactivó "expire_on_commit", tal vez algún estado de la solicitud anterior todavía esté rondando, y ese estado podría incluso ser bastante antiguo. Si tiene cuidado de dejar expire_on_commitencendido y definitivamente llamar commit()o rollback()al final de la solicitud, entonces está bien, pero si comienza con uno nuevo Session, entonces ni siquiera hay duda de que está comenzando limpio. Entonces, la idea de comenzar cada solicitud con un nuevoSessiones realmente la forma más sencilla de asegurarse de que está comenzando de nuevo y de hacer que el uso sea expire_on_commitprácticamente opcional, ya que este indicador puede incurrir en una gran cantidad de SQL adicional para una operación que llama commit()en medio de una serie de operaciones. No estoy seguro de si esto responde a su pregunta.

La siguiente ronda es lo que mencionas sobre el enhebrado. Si su aplicación es multiproceso, le recomendamos que se asegure de que el Sessionuso sea local para ... algo. scoped_session()por defecto lo hace local al hilo actual. En una aplicación web, lo local a la solicitud es incluso mejor. Flask-SQLAlchemy en realidad envía una "función de alcance" personalizada scoped_session()para que obtenga una sesión de alcance de solicitud. La aplicación Pyramid promedio pega la sesión en el registro de "solicitud". Cuando se utilizan esquemas como estos, la idea de "crear una nueva sesión al inicio de la solicitud" sigue pareciendo la forma más sencilla de mantener las cosas en orden.

zzzeek
fuente
17
¡Vaya, esto responde todas mis preguntas sobre la parte de SQLAlchemy e incluso agrega algo de información sobre Flask y Pyramid! Bono adicional: los desarrolladores responden;) Me gustaría poder votar más de una vez. ¡Muchas gracias!
javex
Una aclaración, si es posible: dice que expire_on_commit "puede incurrir en una gran cantidad de SQL adicional" ... ¿puede dar más detalles? Pensé que expire_on_commit solo se refería a lo que sucede en la RAM, no a lo que sucede en la base de datos.
Veky
3
expire_on_commit puede resultar en más SQL si reutiliza la misma sesión nuevamente, y algunos objetos todavía están rondando en esa sesión, cuando acceda a ellos obtendrá un SELECT de una sola fila para cada uno de ellos, ya que cada uno se actualiza individualmente su estado en términos de la nueva transacción.
zzzeek
1
Hola, @zzzeek. Gracias por la excelente respuesta. Soy muy nuevo en Python y varias cosas que quiero aclarar: 1) ¿Entiendo correctamente cuando creo una nueva "sesión" llamando al método Session ()? Se creará una transacción SQL, luego la transacción se abrirá hasta que confirme / deshaga la sesión. ? 2) ¿Session () usa algún tipo de grupo de conexiones o hace una nueva conexión a sql cada vez?
Alex Gurskiy
27

Además de la excelente respuesta de zzzeek, ​​aquí hay una receta simple para crear rápidamente sesiones desechables e independientes:

from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

@contextmanager
def db_session(db_url):
    """ Creates a context with an open SQLAlchemy session.
    """
    engine = create_engine(db_url, convert_unicode=True)
    connection = engine.connect()
    db_session = scoped_session(sessionmaker(autocommit=False, autoflush=True, bind=engine))
    yield db_session
    db_session.close()
    connection.close()

Uso:

from mymodels import Foo

with db_session("sqlite://") as db:
    foos = db.query(Foo).all()
Berislav Lopac
fuente
3
¿Hay alguna razón por la que crea no solo una nueva sesión, sino también una nueva conexión?
danqing
En realidad, no. Este es un ejemplo rápido para mostrar el mecanismo, aunque tiene sentido crear todo lo nuevo en las pruebas, donde más utilizo este enfoque. Debería ser fácil expandir esta función con la conexión como argumento opcional.
Berislav Lopac