This is an interesting question. Here's my explanation of what's going on: When you run a query using a loader option such as raiseload, SQLAlchemy constructs your instance and attaches the "raiseload" behaviour to that instance. In your test_1, this happens, but since you don't store the result, it is immediately garbage collected. You then run a second query, *without* the raiseload option, so SQLAlchemy constructs a second instance and doesn't trigger an exception when you access the "bs" property.
In test_2, you save the result of the first query. It is not garbage collected, so it continues to exist inside SQLAlchemy's identity map, and it has the "raiseload" behaviour. When you run the second query, SQLAlchemy notices that the session *already contains* the instance that you queried for, so it returns that instance. It still has the raiseload behaviour, so you get an exception when you access the "bs" property. So what you are really asking is "should relationship-loading behaviour be stripped from instances already in the session when they are returned from a query which did not specify those options?". I suspect that the answer is no, but I don't know if it is actually specified anywhere. Does that make sense? Simon On Mon, Oct 15, 2018 at 1:49 PM Bartosz Stalewski <bartosz.stalew...@nethone.com> wrote: > > Hi, > > I observed some strange (for me) behavior related to setting query options. I > am not sure if it works as intended or it is a bug. > It seems to me that options are applied to query in a strange manner. The > posted below the code that show this behavior: > 1) in both tests I am configuring sqlalchemy to raise an exception when A.bs > relation is accessed > 2) but in test number 1, I am not assigning result of running query (i.e. I > am not reading it as I understand, since it is lazy) and exception is not > raised. > 3) on the other hand in test number 2, I am assigning this result to variable > 'x' and an exception is raised (this is the only difference between tests). > 4) It does not matter if I run test_1 prior to test_2. > 5) I tested it on current (1.2.12) version of sqlalchemy. > > Documentation > (https://docs.sqlalchemy.org/en/latest/orm/loading_relationships.html#controlling-loading-via-options) > suggests that setting this option should be possible in > the way I am doing it in test 1, but I think that my case shows that it does > not. > > Does it work as it is supposed to work and I am missing something or is it a > bug? > > Creating and populating tables: > > from sqlalchemy import Column, Integer, String, orm, ForeignKey, create_engine > from sqlalchemy.orm import relationship, Session > from sqlalchemy.ext.declarative import declarative_base > > Base = declarative_base() > > > class A(Base): > __tablename__ = 'a' > id = Column(Integer, primary_key=True) > name = Column(String) > > def __repr__(self): return '<A id: {}, name: {}>'.format(self.id, > self.name) > > > class B(Base): > __tablename__ = 'b' > id = Column(Integer, primary_key=True) > > a_id = Column(Integer, ForeignKey('a.id'), index=True) > a = relationship('A', backref='bs') > > def __repr__(self): return '<B id: {}, a_id: {}>'.format(self.id, > self.a_id) > > > e = create_engine("sqlite://", echo=True) > Base.metadata.create_all(e) > > s = Session(e) > s.add(A(id=1, name='A1', bs=[B()])) > s.add(A(id=2, name='A2', bs=[B(), B()])) > s.commit() > > > > Tests definition: > > def test_1(): > # This DOES NOT affect further queries > print('------------------------ Test 1 ------------------------') > s.query(A).options(orm.raiseload(A.bs)).filter_by(id=1).all() > print(s.query(A).filter_by(id=1).all()[0].bs) > > > def test_2(): > # This DOES affect further queries > print('------------------------ Test 2 ------------------------') > x = s.query(A).options(orm.raiseload(A.bs)).filter_by(id=1).all() > print(s.query(A).filter_by(id=1).all()[0].bs) > > > test_1() > test_2() > > > test_1 does not raise any exception, while test_2 raises: > > --------------------------------------------------------------------------- > InvalidRequestError Traceback (most recent call last) > <ipython-input-2-6aad37f5c088> in <module> > 24 create_data() > 25 test_1() > ---> 26 test_2() > > <ipython-input-2-6aad37f5c088> in test_2() > 19 print('------------------------ Test 2 ------------------------') > 20 x = s.query(A).options(orm.raiseload(A.bs)).filter_by(id=1).all() > ---> 21 print(s.query(A).filter_by(id=1).all()[0].bs) > 22 > 23 > > ~/Envs/importer/lib/python3.6/site-packages/sqlalchemy/orm/attributes.py in > __get__(self, instance, owner) > 240 return dict_[self.key] > 241 else: > --> 242 return self.impl.get(instance_state(instance), dict_) > 243 > 244 > > ~/Envs/importer/lib/python3.6/site-packages/sqlalchemy/orm/attributes.py in > get(self, state, dict_, passive) > 597 elif key in state.callables: > 598 callable_ = state.callables[key] > --> 599 value = callable_(state, passive) > 600 elif self.callable_: > 601 value = self.callable_(state, passive) > > ~/Envs/importer/lib/python3.6/site-packages/sqlalchemy/orm/strategies.py in > __call__(self, state, passive) > 832 strategy = prop._strategies[self.strategy_key] > 833 > --> 834 return strategy._load_for_state(state, passive) > 835 > 836 > > ~/Envs/importer/lib/python3.6/site-packages/sqlalchemy/orm/strategies.py in > _load_for_state(self, state, passive) > 587 > 588 if self._raise_always: > --> 589 self._invoke_raise_load(state, passive, "raise") > 590 > 591 session = _state_session(state) > > ~/Envs/importer/lib/python3.6/site-packages/sqlalchemy/orm/strategies.py in > _invoke_raise_load(self, state, passive, lazy) > 562 def _invoke_raise_load(self, state, passive, lazy): > 563 raise sa_exc.InvalidRequestError( > --> 564 "'%s' is not available due to lazy='%s'" % (self, lazy) > 565 ) > 566 > > InvalidRequestError: 'A.bs' is not available due to lazy='raise' > > > -- > SQLAlchemy - > The Python SQL Toolkit and Object Relational Mapper > > http://www.sqlalchemy.org/ > > To post example code, please provide an MCVE: Minimal, Complete, and > Verifiable Example. See http://stackoverflow.com/help/mcve for a full > description. > --- > You received this message because you are subscribed to the Google Groups > "sqlalchemy" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to sqlalchemy+unsubscr...@googlegroups.com. > To post to this group, send email to sqlalchemy@googlegroups.com. > Visit this group at https://groups.google.com/group/sqlalchemy. > For more options, visit https://groups.google.com/d/optout. -- SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper http://www.sqlalchemy.org/ To post example code, please provide an MCVE: Minimal, Complete, and Verifiable Example. See http://stackoverflow.com/help/mcve for a full description. --- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To unsubscribe from this group and stop receiving emails from it, send an email to sqlalchemy+unsubscr...@googlegroups.com. To post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.