SQLAlchemy: Criando vs. Reutilizando uma Sessão

98

Só uma pergunta rápida: SQLAlchemy fala sobre ligar sessionmaker()uma vez, mas ligar para a Session()classe resultante sempre que precisar falar com seu banco de dados. Para mim, isso significa que o segundo que eu faria o meu primeiro session.add(x)ou algo semelhante, eu faria primeiro

from project import Session
session = Session()

O que fiz até agora foi fazer a chamada session = Session()no meu modelo uma vez e depois importar sempre a mesma sessão para qualquer lugar da minha aplicação. Como se trata de um aplicativo da web, isso geralmente significa o mesmo (quando uma visualização é executada).

Mas onde está a diferença? Qual é a desvantagem de usar uma sessão o tempo todo contra usá-la para minhas coisas de banco de dados até que minha função seja concluída e, em seguida, criar uma nova na próxima vez que eu quiser falar com meu banco de dados?

Eu entendo que se eu usar vários threads, cada um deve ter sua própria sessão. Mas usando scoped_session(), já me certifico de que não existe problema, certo?

Por favor, esclareça se alguma das minhas suposições está errada.

javex
fonte

Respostas:

224

sessionmaker()é uma fábrica, ela existe para encorajar a colocação de opções de configuração para a criação de novos Sessionobjetos em apenas um lugar. É opcional, pois você poderia facilmente chamar a Session(bind=engine, expire_on_commit=False)qualquer momento que precisasse de um novo Session, exceto que é prolixo e redundante, e eu queria impedir a proliferação de "ajudantes" em pequena escala, cada um abordando a questão dessa redundância em algum novo e de forma mais confusa.

Portanto, sessionmaker()é apenas uma ferramenta para ajudá-lo a criar Sessionobjetos quando você precisar deles.

Próxima parte. Acho que a questão é: qual é a diferença entre fazer um novo Session()em vários pontos e apenas usar um até o fim. A resposta, não muito. Sessioné um contêiner para todos os objetos que você coloca nele e também mantém o controle de uma transação aberta. No momento em que você chama rollback()ou commit(), a transação termina e o Sessionnão tem conexão com o banco de dados até que seja chamado para emitir SQL novamente. Os links que ele mantém para seus objetos mapeados são referências fracas, desde que os objetos não tenham alterações pendentes, então, mesmo a esse respeito, o Sessionirá esvaziar-se de volta para um novo estado quando seu aplicativo perder todas as referências aos objetos mapeados. Se você deixar com o padrão"expire_on_commit"configuração, todos os objetos expiram após uma confirmação. Se isso Sessiondurar cinco ou vinte minutos e todos os tipos de coisas mudarem no banco de dados na próxima vez que você usá-lo, ele carregará todos os novos estados da próxima vez que você acessar esses objetos, mesmo que eles estejam na memória por vinte minutos.

Em aplicativos da web, costumamos dizer, ei, por que você não faz um novo Sessionem cada solicitação, em vez de usar o mesmo uma e outra vez. Essa prática garante que a nova solicitação comece "limpa". Se alguns objetos da solicitação anterior ainda não foram coletados como lixo e se talvez você tenha desligado "expire_on_commit", talvez algum estado da solicitação anterior ainda esteja por aí, e esse estado pode até ser bem antigo. Se você tiver o cuidado de deixar expire_on_commitligado e ligar definitivamente commit()ou rollback()no final da solicitação, tudo bem, mas se você começar com um novo Session, então não há dúvida de que você está começando limpo. Portanto, a ideia de iniciar cada solicitação com um novoSessioné realmente apenas a maneira mais simples de ter certeza de que você está começando do zero e de tornar o uso de expire_on_commitpraticamente opcional, já que esse sinalizador pode incorrer em muito SQL extra para uma operação que chama commit()no meio de uma série de operações. Não tenho certeza se isso responde à sua pergunta.

A próxima rodada é o que você menciona sobre threading. Se seu aplicativo for multithread, recomendamos certificar-se de que o Sessionem uso é local para ... algo. scoped_session()por padrão, torna-o local para o segmento atual. Em um aplicativo da web, o local da solicitação é, na verdade, ainda melhor. Na verdade, o Flask-SQLAlchemy envia uma "função de escopo" personalizada scoped_session()para que você obtenha uma sessão com escopo de solicitação. O aplicativo Pyramid médio coloca a Sessão no registro de "solicitação". Ao usar esquemas como esses, a ideia "criar uma nova sessão mediante solicitação inicial" continua a parecer a maneira mais direta de manter as coisas em ordem.

zzzeek
fonte
17
Uau, isso responde todas as minhas perguntas sobre a parte SQLAlchemy e até adiciona algumas informações sobre o frasco e a pirâmide! Bônus adicionado: resposta dos desenvolvedores;) Eu gostaria de poder votar mais de uma vez. Muito obrigado!
javex
Um esclarecimento, se possível: você diz expire_on_commit "pode ​​gerar muito SQL extra" ... você pode dar mais detalhes? Pensei que expire_on_commit se preocupasse apenas com o que acontece na RAM, não com o que acontece no banco de dados.
Veky,
3
expire_on_commit pode resultar em mais SQL se você reutilizar a mesma Sessão novamente, e alguns objetos ainda estão pendurados naquela Sessão, quando você os acessa, você obterá um SELECT de uma única linha para cada um deles enquanto cada um é atualizado individualmente seu estado em termos da nova transação.
zzzeek
1
Olá, @zzzeek. Obrigado pela excelente resposta. Eu sou muito novo em python e várias coisas que quero esclarecer: 1) Se eu entendi correto quando eu crio uma nova "sessão" chamando o método Session (), ele criará uma transação SQL, então a transação será aberta até que eu confirme / reverta a sessão ? 2) A sessão () usa algum tipo de pool de conexão ou faz uma nova conexão ao sql a cada vez?
Alex Gurskiy
27

Além da excelente resposta do zzzeek, ​​aqui está uma receita simples para criar rapidamente sessões descartáveis ​​e fechadas:

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
fonte
3
Existe uma razão pela qual você cria não apenas uma nova sessão, mas também uma nova conexão?
danqing
Na verdade, não - este é um exemplo rápido para mostrar o mecanismo, embora faça sentido criar tudo novo nos testes, onde uso essa abordagem com mais frequência. Deve ser fácil expandir essa função com a conexão como um argumento opcional.
Berislav Lopac