Re: [sqlalchemy] session merge sets missing children foreign keys to null

2021-12-08 Thread Mike Bayer


On Wed, Dec 8, 2021, at 10:38 AM, Gabriel Smith wrote:
> Hi, I'm a recent adopter of sqlalchemy, starting fresh with all the 2.0 
> stuff. Thanks so much for the entire teams hard work!

great!

> 
> I have a small issue with the merge functionality and it could honestly just 
> be a misunderstanding of the function from my own point of view. I have a 
> fairly complicated database structure that with 15 nested children tables, 
> some of which have thousands of read-only records. I'd prefer to not have to 
> add all those records to the session in order to run a merge, but when I try 
> to do that, the subsequent flush emits sql updates that set the child foreign 
> keys to null.  
> 
> If there is a way to avoid those updates for orphaned children and just 
> ignore them if they aren't included in the incoming data by primary key, that 
> would really help me, if not, I can look into adding them all to the incoming 
> entity so they'll be ignored.

yeah this is happening because of this line:

   incoming_item.item_prices.append(incoming_price1)

this means you are merging the item like this:

  Item(item_prices = [ItemPrice(...)])

noting that the other two ItemPrice objects are no longer in that collection, 
which is why they are seen as a net remove.Merge doesn't add an incoming 
collection to the one which is already there, it sees the collection coming in 
as "this is the new collection" and replaces the old collection with the new 
one, so it would need to still have the other two objects present within it, 
either as the same persistent prices you already have or as additional 
transient objects with the correct primary key.Below we can do it in the 
former style if we use a future style Session .

This is not a simple case to figure out, as you want to get those price objects 
from persistent and re-merge them, all the while not tripping up and 
accidentally adding the transient "incoming_item" into the session.   this is 
how we can do it:

incoming_item.id = persisted_item.id

# first copy persisted_item's item_prices collection
incoming_item.item_prices = persisted_item.item_prices

# then append to that
incoming_item.item_prices.append(incoming_price1)

but with "legacy" ORM behavior, this will get confused and raise an error since 
it will try to cascade "incoming_item" into the Session prematurely and then 
try to flush it.   There's a way to work around that with make_transient, but 
more succinctly, if we turn on 2.0 behavior for the Session, this unwanted 
cascade is prevented:

Session = sessionmaker(bind=engine, future=True)

that way there isn't even that much overhead to re-merging those existing 
prices since they are already persistent in that session.

As you gave me a full, runnable script, I was able to make those adjustments 
and your assertion at the end succeeds.

great job with the MCVE and paying close attention to the docs as well as 
making our jobs easier!   


> 
> MCVE:
> ```
> from sqlalchemy import Column, Integer, String, ForeignKey, create_engine, 
> select
> from sqlalchemy.orm import registry, declarative_base, relationship, 
> sessionmaker
> from sqlalchemy.sql.sqltypes import Numeric
> import unittest
> 
> Base = declarative_base()
> Mapper_Registry = registry()
> 
> ### MODELS
> 
> @Mapper_Registry.mapped
> class Item:
> __tablename__ = 'item'
>
> id = Column(Integer, *primary_key*=True)
> model_number = Column(String)
> 
> item_prices = relationship("Item_Price", *back_populates*="item", 
> *lazy*="joined")
> 
> @Mapper_Registry.mapped
> class Item_Price:
> __tablename__ = 'item_price'
>
> id = Column(Integer, *primary_key*=True)
> item_id = Column(Integer, ForeignKey('item.id'))
> price = Column(Numeric)
> 
> item = relationship("Item", *back_populates*="item_prices", 
> *lazy*="joined", *viewonly*=True)
> 
> ### TESTS
> 
> class Test_OrphanRecordFKMerge(unittest.TestCase):
> engine = create_engine('sqlite:///:memory:', *echo*=True, 
> *echo_pool*='debug', *future*=True)
> Session = sessionmaker(*bind*=engine)
> session = Session()
> 
> def setUp(*self*):
> Base2 = Mapper_Registry.generate_base()
> Base2.metadata.create_all(self.engine)
> # Create a base item to run tests on
> t_item = Item()
> t_item.model_number = 'TestItem'
> t_price1 = Item_Price()
> t_price1.price = 1.00
> t_item.item_prices.append(t_price1)
> t_price2 = Item_Price()
> t_price2.price = 4.00
> t_item.item_prices.append(t_price2)
> 
> self.session.add(t_item)
> self.session.commit()
> 
> def tearDown(*self*):
> Base.metadata.drop_all(self.engine)
> 
> def test_item_update(*self*):
> self.session.expunge_all()
> # Incoming item data from remote api or flat file
> incoming_item = Item()
> incoming_item.model_number = 'TestItem'
> 

[sqlalchemy] session merge sets missing children foreign keys to null

2021-12-08 Thread Gabriel Smith
Hi, I'm a recent adopter of sqlalchemy, starting fresh with all the 2.0 
stuff. Thanks so much for the entire teams hard work!

I have a small issue with the merge functionality and it could honestly 
just be a misunderstanding of the function from my own point of view. I 
have a fairly complicated database structure that with 15 nested children 
tables, some of which have thousands of read-only records. I'd prefer to 
not have to add all those records to the session in order to run a merge, 
but when I try to do that, the subsequent flush emits sql updates that set 
the child foreign keys to null.  

If there is a way to avoid those updates for orphaned children and just 
ignore them if they aren't included in the incoming data by primary key, 
that would really help me, if not, I can look into adding them all to the 
incoming entity so they'll be ignored.

MCVE:
```
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine, 
select
from sqlalchemy.orm import registry, declarative_base, relationship, 
sessionmaker
from sqlalchemy.sql.sqltypes import Numeric
import unittest

Base = declarative_base()
Mapper_Registry = registry()

### MODELS

@Mapper_Registry.mapped
class Item:
__tablename__ = 'item'

id = Column(Integer, primary_key=True)
model_number = Column(String)

item_prices = relationship("Item_Price", back_populates="item", lazy
="joined")

@Mapper_Registry.mapped
class Item_Price:
__tablename__ = 'item_price'

id = Column(Integer, primary_key=True)
item_id = Column(Integer, ForeignKey('item.id'))
price = Column(Numeric)

item = relationship("Item", back_populates="item_prices", lazy="joined", 
viewonly=True)

### TESTS

class Test_OrphanRecordFKMerge(unittest.TestCase):
engine = create_engine('sqlite:///:memory:', echo=True, echo_pool='debug', 
future=True)
Session = sessionmaker(bind=engine)
session = Session()

def setUp(self):
Base2 = Mapper_Registry.generate_base()
Base2.metadata.create_all(self.engine)
# Create a base item to run tests on
t_item = Item()
t_item.model_number = 'TestItem'
t_price1 = Item_Price()
t_price1.price = 1.00
t_item.item_prices.append(t_price1)
t_price2 = Item_Price()
t_price2.price = 4.00
t_item.item_prices.append(t_price2)

self.session.add(t_item)
self.session.commit()

def tearDown(self):
Base.metadata.drop_all(self.engine)

def test_item_update(self):
self.session.expunge_all()
# Incoming item data from remote api or flat file
incoming_item = Item()
incoming_item.model_number = 'TestItem'
incoming_price1 = Item_Price()
incoming_price1.price = 777.00
incoming_item.item_prices.append(incoming_price1)
# Now we have an incoming item, we need to query the database for 
the existing item and reconcile the primary keys
# so that it can be updated correctly
persisted_item = 
self.session.execute(select(Item).where(Item.model_number == 
'TestItem')).scalars().first()
incoming_item.id = persisted_item.id
# let us imagine that the new price should not overwrite either old 
price 
self.session.merge(incoming_item)
self.session.commit()
self.session.expunge_all()
final_result = 
self.session.execute(select(Item).where(Item.model_number == 
'TestItem')).scalars().first()
# the following test fails as both the other price records have had 
their foreign keys set to null after the merge
# so the len(final_result.item_prices) == 1
self.assertEqual(len(final_result.item_prices), 3)

if __name__ == '__main__':
unittest.main()
```

Output:
```
2021-12-08 09:34:46,053 INFO sqlalchemy.engine.Engine UPDATE item_price SET 
item_id=? WHERE item_price.id = ?
2021-12-08 09:34:46,053 INFO sqlalchemy.engine.Engine [generated in 
0.00056s] ((None, 1), (None, 2))
2021-12-08 09:34:46,055 INFO sqlalchemy.engine.Engine INSERT INTO 
item_price (item_id, price) VALUES (?, ?)
2021-12-08 09:34:46,055 INFO sqlalchemy.engine.Engine [cached since 
0.03964s ago] (1, 777.0)
2021-12-08 09:34:46,057 INFO sqlalchemy.engine.Engine COMMIT
```

The update statement above is what I am trying to avoid, but I'd still like 
to use the merge functionality if possible.

Thanks for any guidance and for all you've put into this amazing library.

-- 
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 

Re: [sqlalchemy] Re: How to add the index_elements to the on_conflict_do_update() method

2021-12-08 Thread Simon King
Does the table definition in postgres match your SQLAlchemy
definition? Adding "unique=True" to the SQLAlchemy table definition
will not automatically add an index to an existing table in the
database.

If you connect to the database using "psql" and run "\d
message_symbol", does it show the primary key and/or unique index?

Simon

On Tue, Dec 7, 2021 at 10:27 PM Chaozy Z  wrote:
>
> I also tried to add unique=True to the column message_id but still fail with 
> the same error
>
> On Tuesday, 7 December 2021 at 22:21:49 UTC Chaozy Z wrote:
>>
>> Hi there, I just started to learn SQLAlchemy. 0
>>
>> I have a `on_conflict_do_update` command as follows:
>>
>> ```
>>
>> insert_stmt = insert(MessageSymbol).values(message_id=12345, symbol_id=1)
>>
>> do_update_stmt = insert_stmt.on_conflict_do_update( 
>> index_elements=['message_id'], set_=dict( symbol_id=123 ) )
>>
>> ```
>>
>> and my MessageSymbol is defined as follow:
>>
>> ```
>>
>> class MessageSymbol(Base):
>>
>>  __tablename__ = "message_symbol"
>>
>>
>> message_id = Column(BigInteger, primary_key=True, nullable=False)
>>
>>
>> symbol_id = Column(BigInteger, nullable=False)
>>
>> ```
>>
>> When the command is executed it throws the error:
>>
>> ```
>>
>> sqlalchemy.exc.ProgrammingError: (psycopg2.errors.InvalidColumnReference) 
>> there is no unique or exclusion constraint matching the ON CONFLICT 
>> specification
>>
>> [SQL: INSERT INTO message_symbol (message_id, symbol_id) VALUES 
>> (%(message_id)s, %(symbol_id)s) ON CONFLICT (message_id) DO UPDATE SET 
>> symbol_id = %(param_1)s]
>> [parameters: {'message_id': 12345, 'symbol_id': 1, 'param_1': 123}]
>>
>> ```
>>
>> Since I have defined the `message_id` as the primary key I assume it should 
>> be a unique constraint. I am wondering what else is being missing?
>>
>> Chaozy
>
> --
> 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/734ae718-fde8-4b54-9de9-b2d81698a0dfn%40googlegroups.com.

-- 
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/CAFHwexem_JXPsNS_1_rqCPb1FS8MFLViMt0C3q_1gctD7SFrOw%40mail.gmail.com.