On Fri, Sep 11, 2020, at 2:26 AM, Jacob Pavlock wrote:
> 
> Mike,
> 
> Thanks for the great explanation and the quick response! I think am I 
> starting to get it now. I guess the same or similar idea though leads to the 
> following (somewhat confusing) behavior:
> 
> (I added a repr and setters for the hybrid properties)
> 
> `track1 = Track(album="tpab", albumartist="K.dot", track_num=1, year=2015)
track2 = Track(album="tpab", albumartist="K.dot", track_num=1, year=2015)

track2.album = "gkmc"
session.merge(track1)
session.merge(track2)
session.commit()

print(session.query(Track).all())
`
> 
> 
> Results in 
> 
> `[K.dot - gkmc (2015): 1]
`
> 
> 
> 
> But
> 
> `track1 = Track(album="tpab", albumartist="K.dot", track_num=1, year=2015)
track2 = Track(album="tpab", albumartist="K.dot", track_num=1, year=2015)

track1.album = "gkmc"
session.merge(track1)
session.merge(track2)
session.commit()

print(session.query(Track).all())
`
> 
> 
> Results in 
> 
> `[K.dot - gkmc (2015): 1, K.dot - tpab (2015): 1]
`
> 
> 
> I feel like I'm starting to tread in non-standard waters here, and I am 
> beginning to question whether my use of `session.merge()` is appropriate for 
> my usecase. Any recommendations?

merge() is just a little weird in this regard, it's not my first go-to method 
for sure. it's based on the same idea in Hibernate.  I would not have ever 
thought of such a method on my own.

The specific issue in your example above is that you are setting "track1.album 
= "gkmc"", but you have this attribute set to be populated from that of your 
Album() object which I would assume is overwriting it during the flush.

that is, you have an Album object on your Track, it doesn't actually make sense 
to change track1.album = "foo" without changing that whole object.   

I guess what most people would do here would be to not use natural primary keys 
(e.g. use a surrogate primary key, like an integer).    your table rows would 
use less data and the model would be simpler to manipulate.


> On Thursday, September 10, 2020 at 10:44:09 PM UTC-5 Mike Bayer wrote:
>> __
>> When you create a Track object and don't assign the primary key attributes 
>> directly, the instance key of the object is all nulls:
>> 
>> Track.__mapper__.identity_key_from_instance(track1)
>> (<class '__main__.Track'>, (1, None, None, None), None)
>> 
>> Track.__mapper__.identity_key_from_instance(track2)
>> (<class '__main__.Track'>, (1, None, None, None), None)
>> 
>> this is because while the assignment of _album_obj will set the primary key 
>> attributes, that doesn't happen until the flush, so right now Track has no 
>> primary key.
>> 
>> merge() sees the first object, puts it in as a new object that's pending 
>> without a primary key.
>> 
>> then the second merge() call occurs. a flush proceeds first, which flushes 
>> track1, assigning the _album_obj which sets up the primary key attributes 
>> appropriately on track1.    track2 comes in, and the apparent primary key is 
>> again all nulls, so it again goes in as a pending object to be inserted.   
>> the commit tries to flush and it fails because the object's primary key once 
>> calculated by _album_obj conflicts with what's in the database.  merge() had 
>> no idea you intended this to be the same primary key identity because those 
>> attributes are not set up front.
>> 
>> When you explicitly set up the three pk attributes on Track, then the 
>> merge() process can see the apparent primary key you had in mind and it can 
>> correctly match "track2" with the "track1" that's already in the database.
>> 
>> hope this helps!
>> 
>> 
>> 
>> 
>> 
>> 
>> On Thu, Sep 10, 2020, at 9:44 PM, Jacob Pavlock wrote:
>>> Hello! I am new to sqlalchemy and was hoping to get some insight on the 
>>> following issue:
>>> 
>>> `*import* sqlalchemy
>>> *from* sqlalchemy *import* Column, Integer, String
>>> *from* sqlalchemy.ext.declarative *import* declarative_base
>>> *from* sqlalchemy.ext.hybrid *import* hybrid_property
>>> *from* sqlalchemy.orm *import* relationship, sessionmaker
>>> *from* sqlalchemy.schema *import* ForeignKeyConstraint

Session = sessionmaker()
Base = declarative_base()

>>> *class* *Album*(Base):

    __tablename__ = "albums"

    artist = Column(String, nullable=*False*, primary_key=*True*)
    title = Column(String, nullable=*False*, primary_key=*True*)
    year = Column(Integer, nullable=*False*, primary_key=*True*)

    tracks = relationship(
        "Track", back_populates="_album_obj", cascade="all, delete-orphan"
    )

    *def* *__init__*(self, artist: str, title: str, year: int):
        self.artist = artist
        self.title = title
        self.year = year

>>> *class* *Track*(Base):  *# noqa: WPS230*
    __tablename__ = "tracks"

    *# track_num + Album = unique track*
    track_num = Column(Integer, nullable=*False*, primary_key=*True*, 
autoincrement=*False*)
    _albumartist = Column(String, nullable=*False*, primary_key=*True*)
    _album = Column(String, nullable=*False*, primary_key=*True*)
    _year = Column(Integer, nullable=*False*, primary_key=*True*, 
autoincrement=*False*)

    artist = Column(String, nullable=*False*, default="")
    title = Column(String, nullable=*False*, default="")

    _album_obj = relationship("Album", back_populates="tracks")

    __table_args__ = (
        ForeignKeyConstraint(
            [_albumartist, _album, _year],  *# type: ignore*
            [Album.artist, Album.title, Album.year],
        ),
    )

    @hybrid_property
    *def* *album*(self) -> *str*:
        *return* self._album_obj.title

    @hybrid_property
    *def* *albumartist*(self) -> *str*:
        *return* self._album_obj.artist

    @hybrid_property
    *def* *year*(self) -> *int*:
        *return* self._album_obj.year

    *def* *__init__*(self, album: str, albumartist: str, track_num: int, year: 
int):
        self._album_obj = Album(artist=albumartist, title=album, year=year)
        self.track_num = track_num
        *# self._album = album*
        *# self._albumartist = albumartist*
        *# self._year = year*

engine = sqlalchemy.create_engine("sqlite:///:memory:", echo=*True*)
Session.configure(bind=engine)
Base.metadata.create_all(engine)  *# creates tables*
session = Session()

track1 = Track(album="tpab", albumartist="K.dot", track_num=1, year=2015)
track2 = Track(album="tpab", albumartist="K.dot", track_num=1, year=2015)

session.merge(track1)
session.merge(track2)
session.commit()
`
>>> 
>>> When executing the above code, I get 
>>> 
>>> `Traceback (most recent call last):
  File "test_merge.py", line 81, in <module>
    session.commit()
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py",
 line 1042, in commit
    self.transaction.commit()
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py",
 line 504, in commit
    self._prepare_impl()
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py",
 line 483, in _prepare_impl
    self.session.flush()
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py",
 line 2523, in flush
    self._flush(objects)
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py",
 line 2664, in _flush
    transaction.rollback(_capture_exception=True)
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py",
 line 68, in __exit__
    compat.raise_(
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/util/compat.py",
 line 182, in raise_
    raise exception
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py",
 line 2624, in _flush
    flush_context.execute()
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/unitofwork.py",
 line 422, in execute
    rec.execute(self)
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/unitofwork.py",
 line 586, in execute
    persistence.save_obj(
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/persistence.py",
 line 205, in save_obj
    for (
  File 
"/home/jacob/src/moe/.venv/lib/python3.8/site-packages/sqlalchemy/orm/persistence.py",
 line 405, in _organize_states_for_save
    raise orm_exc.FlushError(
sqlalchemy.orm.exc.FlushError: New instance <Track at 0x7f72ccd17f70> with 
identity key (<class '__main__.Track'>, (1, 'K.dot', 'tpab', 2015), None) 
conflicts with persistent instance <Track at 0x7f72ccc901c0>
`
>>> 
>>> But, If I uncomment
>>> 
>>> `        *# self._album = album*
        *# self._albumartist = albumartist*
        *# self._year = year*
`
>>> 
>>> The code works fine. I don't quite understand why the above code is needed, 
>>> and happened to add it just by random testing to see if I could pin down 
>>> the error. I think the  section in the docs on merge tips 
>>> <https://docs.sqlalchemy.org/en/13/orm/session_state_management.html#merge-tips>
>>>  is the closet thing I could find, but I can't quite understand if the same 
>>> thing there is happening here. Any insight would be greatly appreciated!
>>> 

>>> --
>>> 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 [email protected].
>>> To view this discussion on the web visit 
>>> https://groups.google.com/d/msgid/sqlalchemy/8f3ecc9d-8ab3-407d-b946-ef67aa21a3afn%40googlegroups.com
>>>  
>>> <https://groups.google.com/d/msgid/sqlalchemy/8f3ecc9d-8ab3-407d-b946-ef67aa21a3afn%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 [email protected].
> To view this discussion on the web visit 
> https://groups.google.com/d/msgid/sqlalchemy/98ab5a17-da18-4b88-9dc2-fcf650bbd5c6n%40googlegroups.com
>  
> <https://groups.google.com/d/msgid/sqlalchemy/98ab5a17-da18-4b88-9dc2-fcf650bbd5c6n%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 [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/sqlalchemy/e7220db3-7aa1-4d3b-a579-aac73d935abc%40www.fastmail.com.

Reply via email to