On 12/07/2016 12:00 PM, 'dcs3spp' via sqlalchemy wrote:
Hi Mike,

Thanks for getting back to me and suggesting viable workarounds :) Will
probably go with the event listener approach.
Is it worthwhile submitting this as an issue on bitbucket?


I just looked a little more closely here and realized the reason this isn't a dupe is because we are putting the same object identity into the collection. That is, even the one-to-many version would have no net insert if you did something like this:

a1 = A(bs=[B(some_unique_thing=1)])
s.add(a1)
s.commit()

b1 = s.query(B).first()

a1.bs.append(b1)
s.commit()

e.g., a1.bs is [b1, b1], same in-memory identity:

(Pdb) a1.bs
[<__main__.B object at 0x7f2fb38c93d0>, <__main__.B object at 0x7f2fb38c93d0>]


The attribute change mechanics, which tell the unit of work what new rows to insert, necessarily work against object identity in order to detect a net append, so this is why in the many-to-many case, adding the same object twice is also ignored. so I don't think this specific case can be changed; at best we detect at the collection level which is what the event is doing for you.






Cheers

Simon

On Tuesday, 6 December 2016 14:36:15 UTC, dcs3spp wrote:

    Hi,


    I am running SQLAlchemy 1.1.4 connected to postgreSQL 9.6 on OSX
    Sierra 10.12.1. I am experiencing difficulty getting SQLAlchemy to
    raise an integrity error when a persistent object is appended to a
    relationship for which it is already associated with.



    Is the accepted method to manually raise an exception by testing for
    existing membership in the collection_class of the relationship
    before inserting the persistent object into the relationship? I have
    tested with default list and also setting the collection_class to
    set. Would be grateful if anyone could help with this and point out
    where I am going wrong :)


    I have included details of code and DB structure below. Currently,
    duplicate associations are silently ignored, i.e. no INSERT
    statements generated.


    Cheers


    Simon




    DB Structure

    Assignment —< AssignmentOutcome >— LearningOutcome


    TABLE KEYS

    Assignment identified by AssignmentID

    LearningOutcome identified by CourseID, UnitID, OutcomeID

    AssignmentOutcome identified by AssignmentID, CourseID, UnitID,
    OutcomeID


    ASSOCIATION TABLE STATE BEFORE TEST SCRIPT RUN

    proposed_coursemanagement=> SELECT * FROM assignment outcome;

     AssignmentID | CourseID | UnitID | OutcomeID

    --------------+----------+--------+-----------

                1 |  1014760 |      2 |         1

                1 |  1014760 |      2 |         2

                2 |  1014760 |      2 |         1

                2 |  1014760 |      2 |         3

                3 |  1014760 |      2 |         2

                4 |  1014760 |      2 |         3

    (6 rows)


    TEST SCRIPT PURPOSE: Try appending outcome 1 again to assignment 1.
    Should SQLAlchemy raise and exception since this is already mapped
    in association table? Current behaviour is that duplicate is
    silently ignored. When I append outcome 4 for assignment 1 it is
    mapped to the association table.


    TEST SCRIPT CODE:

    def usage(argo):

        cmd = os.path.basename(argv[0])

        print('usage: %s <config_uri> [var=value]\n'

              '(example: "%s development.ini")' % (cmd, cmd))


    def main(argv=sys.argv):



        if len(argv) < 2:

            usage(argv)


        print ("Initialising SQLAlchemy engine instance and tables.......")

        config_uri = argv[1]

        options = parse_vars(argv[2:])

        setup_logging(config_uri)

        settings = get_appsettings(config_uri, options=options)


        engine = get_engine(settings)

        Base.metadata.create_all(engine)

        session_factory = get_session_factory(engine)

        print ("Completed initialisation.....")

        print ("Starting Test Script...")

        with transaction.manager:

            dbsession = get_tm_session(session_factory, transaction.manager)


            # load into session assignment 1 and learning outcomes 1 and 4

            # outcome 1 already exists in the association table for
    assignment 1

            # outcome 4 does not exist yet in the association table for
    assignment 1

            assignment = dbsession.query(AssignmentModel).get(1)

            outcome_1 =
    dbsession.query(LearningOutcomeModel).get((1014760, 2, 1))

            outcome_4 =
    dbsession.query(LearningOutcomeModel).get((1014760, 2, 4))


            # add outcome 1 and outcome 4 to assignmentoutcomes relationship

            assignment.assignmentoutcomes.append(outcome_1)

            assignment.assignmentoutcomes.append(outcome_4)


            # inspect and display state for assignment and outcome objects

            assignmentIns = inspect (assignment)

            outcome1Ins = inspect (outcome_1)

            outcome4Ins = inspect (outcome_4)

            print("assignment object=Transient: {0}, Pending: {1},
    Persistent: {2}, Detatched: {3}".format(

                assignmentIns.transient, assignmentIns.pending,
    assignmentIns.persistent, assignmentIns.detached))

            print("outcome_1 object=Transient: {0}, Pending: {1},
    Persistent: {2}, Detatched: {3}".format(

                outcome1Ins.transient, outcome1Ins.pending,
    outcome1Ins.persistent, outcome1Ins.detached))

            print("outcome_4 object=Transient: {0}, Pending: {1},
    Persistent: {2}, Detatched: {3}".format(

                outcome4Ins.transient, outcome4Ins.pending,
    outcome4Ins.persistent, outcome4Ins.detached))


            # inspect and display session states .dirty, .new and .deleted

            print ("dbsession.dirty={0}, .new={1}, .deleted={2}".format(

                dbsession.dirty, dbsession.new, dbsession.deleted))

        print ("Test Script complete...")


    TEST SCRIPT OUTPUT

    assignment object=Transient: False, Pending: False, Persistent:
    True, Detatched: False

    outcome_1 object=Transient: False, Pending: False, Persistent: True,
    Detatched: False

    outcome_4 object=Transient: False, Pending: False, Persistent: True,
    Detatched: False

    dbsession.dirty=IdentitySet([<LearningOutcome('1014760','2', '4',
    'Unit 2 : Learning Outcome 4')>, <LearningOutcome('1014760','2',
    '1', 'Understand the components of computer systems')>,
    <Assignment('1','1014760', '2', '2015-12-10 00:00:00+00:00',
    'Assignment 1')>]), .new=IdentitySet([]), .deleted=IdentitySet([])

    2016-12-06 12:33:10,691 INFO
    [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO
    proposed_coursemanagement.assignmentoutcome ("CourseID", "UnitID",
    "OutcomeID", "AssignmentID") VALUES (%(CourseID)s, %(UnitID)s,
    %(OutcomeID)s, %(AssignmentID)s)

    2016-12-06 12:33:10,691 INFO
    [sqlalchemy.engine.base.Engine:1100][MainThread] {'OutcomeID': 4,
    'CourseID': 1014760, 'UnitID': 2, 'AssignmentID': 1}

    2016-12-06 12:33:10,692 INFO
    [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT

    Test Script complete...




    ASSOCIATION TABLE STATE AFTER TEST SCRIPT RUN

    proposed_coursemanagement=> SELECT * FROM assignmentoutcome;

     AssignmentID | CourseID | UnitID | OutcomeID

    --------------+----------+--------+-----------

                1 |  1014760 |      2 |         1 * No duplicate raised
    - does SQLAlchemy silently ignore?

                1 |  1014760 |      2 |         2

                2 |  1014760 |      2 |         1

                2 |  1014760 |      2 |         3

                3 |  1014760 |      2 |         2

                4 |  1014760 |      2 |         3

                1 |  1014760 |      2 |         4

    (7 rows)



    MODEL CODE:


    assignmentoutcomeallocation = Table('assignmentoutcome', Base.metadata,

        Column('CourseID', BigInteger, primary_key=True),

        Column('UnitID', Integer, primary_key=True),

        Column('OutcomeID', Integer, primary_key=True),

        Column('AssignmentID', BigInteger,
    ForeignKey('assignment.AssignmentID'), primary_key=True),

        UniqueConstraint('CourseID', 'UnitID', 'OutcomeID', 'AssignmentID'),

        ForeignKeyConstraint(

            ['CourseID', 'UnitID', 'OutcomeID'],

            ['learningoutcome.CourseID', 'learningoutcome.UnitID',
    'learningoutcome.OutcomeID']),

    )


    class AssignmentModel(Base):

        __tablename__ = 'assignment'

        __table_args__ = (

                ForeignKeyConstraint(['CourseID','UnitID'],
    ['unit.CourseID', 'unit.UnitID']),

            )


        AssignmentID = Column(BigInteger, primary_key=True,


    
server_default=text("nextval('proposed_coursemanagement.\"assignment_AssignmentID_seq\"::regclass)"))

        CourseID = Column(BigInteger, nullable=False)

        UnitID = Column(Integer, nullable=False)

        AssignmentSetDate = Column(DateTime(timezone=True), nullable=False)

        AssignmentSubmissionDate = Column(DateTime(timezone=True),
    nullable=False)

        AssignmentResubmissionDate = Column(DateTime(timezone=True),
    nullable=False)

        AssignmentTitle = Column(String(200), nullable=False)


        indx = Index('assignment_indx', 'CourseID', 'UnitID',
    text('lower(\"AssignmentTitle\")'), 'AssignmentSetDate')


        unit = relationship('UnitModel')

        assignmentcriteria = relationship('UnitCriteriaModel',


    secondary=assignmentcriteriaallocation,


    back_populates='criteria_assignments')

        assignmentoutcomes = relationship('LearningOutcomeModel',


    secondary=assignmentoutcomeallocation,




    back_populates='outcome_assignments')



        def __repr__(self):

            return "<Assignment('%s','%s', '%s', '%s', '%s')>" %
    (self.AssignmentID, self.CourseID, self.UnitID,
    self.AssignmentSetDate, self.AssignmentTitle)


        def __str__(self):

            return "<Assignment('%s','%s', '%s', '%s', '%s')>" %
    (self.AssignmentID, self.CourseID, self.UnitID,
    self.AssignmentSetDate, self.AssignmentTitle)



    class LearningOutcomeModel(Base):

        __tablename__ = 'learningoutcome'

        __table_args__ = (

                ForeignKeyConstraint(['CourseID','UnitID'],
    ['unit.CourseID', 'unit.UnitID']),

            )


        CourseID = Column(BigInteger, primary_key=True, nullable=False)

        UnitID = Column(Integer, primary_key=True, nullable=False)

        OutcomeID = Column(Integer, primary_key=True, nullable=False)

        LearningOutcome = Column(String(100), nullable=False)


        unit = relationship('UnitModel')

        outcome_assignments = relationship (

                'AssignmentModel',

                secondary=assignmentoutcomeallocation,

                back_populates='assignmentoutcomes')


        def __repr__(self):

            return "<LearningOutcome('%s','%s', '%s', '%s')>" %
    (self.CourseID, self.UnitID, self.OutcomeID, self.LearningOutcome)


        def __str__(self):

            return "<LearningOutcome('%s','%s', '%s', '%s')>" %
    (self.CourseID, self.UnitID, self.OutcomeID, self.LearningOutcome)


--
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
<mailto:sqlalchemy+unsubscr...@googlegroups.com>.
To post to this group, send email to sqlalchemy@googlegroups.com
<mailto: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