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.

Reply via email to