Hi Mike

Thanks very much for all the feedback, 'expunging the expunges' and setting 
expire_on_commit=False worked a treat. Had to add commits for read 
operations too, which felt a little awkward, but works nicely. 

Nathan


On Monday, 16 May 2022 at 19:19:56 UTC+1 Mike Bayer wrote:

> oh an additional clarifying point.  if you are seeing that error message 
> happen on a detached object, which seems to go away when you "expunge()" 
> the object sooner, that's because your session is expiring on commit.  if 
> your application works with detached objects, you must set expire_on_commit 
> to False.    See the docs recently clarified at 
> https://docs.sqlalchemy.org/en/14/orm/session_basics.html#session-committing 
> .
>
> On Mon, May 16, 2022, at 2:14 PM, Mike Bayer wrote:
>
> that error message still describes an object that isn't associated with a 
> Session.   If you organize your application to deal with objects only 
> within the scope of a single session, and never once that session has been 
> closed, you won't get that error message.  
>
>
>
> On Mon, May 16, 2022, at 12:07 PM, Nathan Johnson wrote:
>
> Hi Mike
>
> Thanks a lot for taking a look and your speedy reply.
>
> The reason it was being used in this context is that it's the only way I 
> could get the proxied attribute to resolve after the session had closed, 
> despite the relationship having `lazy='subquery'` specified i.e. eager 
> loading.
>
> Without the expunge, attempting to access the `lookup` attribute outside 
> of the session results in:
>
> ```
>   File 
> "/usr/local/lib/python3.8/site-packages/sqlalchemy/ext/associationproxy.py", 
> line 193, in __get__
>     return inst.get(obj)
>   File 
> "/usr/local/lib/python3.8/site-packages/sqlalchemy/ext/associationproxy.py", 
> line 546, in get
>     target = getattr(obj, self.target_collection)
>   File 
> "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 
> 465, in __get__
>     return self.impl.get(state, dict_)
>   File 
> "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 
> 911, in get
>     value = self.callable_(state, passive)
>   File 
> "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/strategies.py", line 
> 832, in _load_for_state
>     raise orm_exc.DetachedInstanceError(
> sqlalchemy.orm.exc.DetachedInstanceError: Parent instance <Dave at 
> 0x7f974f015fd0> is not bound to a Session; lazy load operation of attribute 
> '_lookup' cannot proceed (Background on this error at: 
> http://sqlalche.me/e/14/bhk3)
> ```
>
> Thanks
>
> Nathan
> On Wednesday, 11 May 2022 at 15:25:11 UTC+1 Mike Bayer wrote:
>
>
> the pattern you are using with expunge() is likely creating confusion as 
> to what objects are still in the Session and which ones aren't.      I 
> would seek to organize your application such that the entire sequence of 
> load/manipulation operations with a set of objects proceeds under a single 
> Session() instance; when all is complete and the transaction is done, you 
> can then .close() that session which will expunge all objects.  if the 
> objects are then being passed to a view layer etc., they can opearate in a 
> read-only fashion.  
>
> otherwise, add()ing objects back to a session that were just expunged is 
> usually an antipattern unless there is a very specific use for it (such as 
> passing objects between workers), in which case it has to be done very 
> carefully.  the .expunge() method should never be part of any broadly-used 
> pattern.
>
> there's more guidelines on Session use at 
> https://docs.sqlalchemy.org/en/14/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it
>  
>
> On Wed, May 11, 2022, at 6:24 AM, Nathan Johnson wrote:
>
> Hi
>
> I'm attempting to use an association_proxy approach to support a look up 
> table with classical mapping.
>
> The problem I'm having is that attempting to update/add an existing object 
> to a session causes:
>
> ------
> Traceback (most recent call last):
>   File "association_proxy_poc.py", line 118, in <module>
>     add_with_lookup_association_proxy(session, read_obj)
>   File "association_proxy_poc.py", line 80, in 
> add_with_lookup_association_proxy
>     session.add(obj)
>   File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/session.py", 
> line 2530, in add
>     self._save_or_update_state(state)
>   File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/session.py", 
> line 2549, in _save_or_update_state
>     self._save_or_update_impl(st_)
>   File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/session.py", 
> line 3095, in _save_or_update_impl
>     self._update_impl(state)
>   File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/session.py", 
> line 3084, in _update_impl
>     self.identity_map.add(state)
>   File 
> "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/identity.py", line 
> 148, in add
>     raise sa_exc.InvalidRequestError(
> sqlalchemy.exc.InvalidRequestError: Can't attach instance <LookUp at 
> 0x7fb96ce55730>; another instance with key (<class '__main__.LookUp'>, 
> (7,), None) is already present in this session.
> ---------
>
> This is my example code, apologies for the lack of highlighting (pasting 
> from  VSCode normally does this in gmail, but apparently not google 
> groups). This works as expected until line 118 (annotated below).
>
> -------------------------
>
> class LookUp():
>     def __init__(self, lookup_value: str):
>         self.lookup_value = lookup_value
>
>
> class Dave:
>     def __init__(self, lookup: str, id: int = None, updatable: str = None):
>         self.id = id
>         self.lookup = lookup
>         self.updatable = updatable
>
>
> mapper_registry = registry()
>
> lookup_table = Table(
>     'lookup',
>     mapper_registry.metadata,
>     Column('id', SmallInteger, primary_key=True),
>     Column('lookup_value', String(36), unique=True)
> )
>
> dave_table = Table(
>     'dave',
>     mapper_registry.metadata,
>     Column('id', INTEGER(unsigned=True), primary_key=True),
>     Column('updatable', String(36)),
>     Column('lookup_id', SmallInteger, ForeignKey('lookup.id'))
> )
>
> mapper_registry.map_imperatively(LookUp, lookup_table)
> mapper_registry.map_imperatively(
>     Dave,
>     dave_table,
>     properties={
>         '_lookup': relationship(LookUp, uselist=False, lazy='subquery', 
> cascade='expunge, save-update, merge'),
>     }
> )
> Dave.lookup = association_proxy('_lookup', 'lookup_value')
>
>
> from sqlalchemy import create_engine
> from sqlalchemy.orm import sessionmaker
> config = DbSettings()
> conn_str = config.db_conn_str
> engine = create_engine(conn_str, echo=True, pool_pre_ping=True)
> _sessionmaker = sessionmaker(autocommit=False, autoflush=False, 
> bind=engine)
>
>
> def add_with_lookup_association_proxy(session, obj):
>     if lookup := 
> session.query(LookUp).filter_by(lookup_value=obj.lookup).one_or_none():
>         # Ensures we re-use exisitng LookUp records
>         print(f"################## Re-using lookup {lookup}")
>         obj._lookup = lookup
>
>     session.add(obj)
>     session.flush()
>     session.expunge(obj)
>     return obj
>
>
> def read_with_lookup_association_proxy(session, id, lookup):
>     query = session.query(Dave).filter_by(id=id, lookup=lookup)
>     obj = query.one()
>     session.expunge(obj)
>     return obj
>
> lookup = 'SOME HIGHLY REDUNDANT VALUE'
>
> with _sessionmaker() as session:
>     new_obj = Dave(lookup=lookup)
>     add_with_lookup_association_proxy(session, new_obj)
>     session.commit()
>
> print(f"############## NEW {new_obj.lookup}")
> print(new_obj.lookup_id)
>
> with _sessionmaker() as session:
>     read_obj = read_with_lookup_association_proxy(session, new_obj.id, 
> new_obj.lookup)
>     print(f"############## READ {read_obj.lookup}")
>     read_obj.updatable = 'UPDATED'
>     add_with_lookup_association_proxy(session, read_obj)  # line 118 This 
> line triggers the error
>     session.commit()
>
> with _sessionmaker() as session:
>     updated_obj = read_with_lookup_association_proxy(session, new_obj.id, 
> new_obj.lookup)
>     print(f"########## READ UPDATED {updated_obj.updatable}")
>
> ----------------
>
> I have played around with the omitting the save-update cascade and adding 
> the obj._lookup to the session directly, but this results in:
>
> ----
> /usr/local/lib/python3.8/site-packages/sqlalchemy/orm/dependency.py:835: 
> SAWarning: Object of type <LookUp> not in session, add operation along 
> 'Dave._lookup' won't proceed
> ----
>
> Would really appreciate some insight as to what I'm getting wrong here.
>
> Thanks
>
> Nathan
>
>
> -- 
> 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+...@googlegroups.com.
> To view this discussion on the web visit 
> https://groups.google.com/d/msgid/sqlalchemy/377ed850-d53e-4253-a43d-2ddfe04d8af5n%40googlegroups.com
>  
> <https://groups.google.com/d/msgid/sqlalchemy/377ed850-d53e-4253-a43d-2ddfe04d8af5n%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>
>
>
> -- 
> 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+...@googlegroups.com.
> To view this discussion on the web visit 
> https://groups.google.com/d/msgid/sqlalchemy/b965aceb-8829-4e1b-b650-f7016601b6dcn%40googlegroups.com
>  
> <https://groups.google.com/d/msgid/sqlalchemy/b965aceb-8829-4e1b-b650-f7016601b6dcn%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>
>
>
> -- 
> 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+...@googlegroups.com.
>
> To view this discussion on the web visit 
> https://groups.google.com/d/msgid/sqlalchemy/27492fcd-e346-4b76-8d4e-69c609b48ca0%40www.fastmail.com
>  
> <https://groups.google.com/d/msgid/sqlalchemy/27492fcd-e346-4b76-8d4e-69c609b48ca0%40www.fastmail.com?utm_medium=email&utm_source=footer>
> .
>
>
>

-- 
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 view this discussion on the web visit 
https://groups.google.com/d/msgid/sqlalchemy/b270181d-43e1-49cc-8069-c1a961364211n%40googlegroups.com.

Reply via email to