Ok, a complete MCVE looks like that:

from sqlalchemy import Column, ForeignKey, Integer, String, create_engine, event
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship, sessionmaker

Base = declarative_base()


class Entity(Base):
    __tablename__ = 'entity'
    id = Column(Integer, primary_key=True)
    tags = relationship('TagAssociation', backref='entity',
                        cascade="all, delete, delete-orphan")


class Vocabulary(Base):
    __tablename__ = 'vocabulary'
    id = Column(Integer, primary_key=True)
    name = Column(String(255), nullable=False, unique=True)


class Tag(Base):
    __tablename__ = 'tag'
    id = Column(Integer, primary_key=True)
    vocabularyId = Column(Integer, ForeignKey(Vocabulary.id), nullable=False)
    vocabulary = relationship(Vocabulary, lazy='joined',
                              backref=backref('tags', cascade='all, 
delete-orphan'))
    name = Column(String(50), nullable=False)


class TagAssociation(Base):
    __tablename__ = 'tag_association'
    entityId = Column(Integer, ForeignKey(Entity.id), primary_key=True)
    tagId = Column(Integer, ForeignKey(Tag.id), primary_key=True)
    tag = relationship(Tag, lazy='joined')


@event.listens_for(TagAssociation, 'before_insert')
def validateOnlyOneTagPerVocabulary(mapper, connection, assoc):
    vocabulary = assoc.tag.vocabulary
    existingAssocs = session.query(TagAssociation).join(Tag) \
        .filter(Tag.vocabulary == vocabulary, TagAssociation.entity == 
assoc.entity)
    newAssocs = [assoc for assoc in assoc.entity.tags
                 if assoc in session.new and assoc.tag.vocabulary == vocabulary]
    if len(newAssocs) > 1 or (len(newAssocs) + existingAssocs.count()) > 1:
        raise ValueError('Can only assign one tag of vocabulary "%s".' % 
vocabulary.name)


engine = create_engine('sqlite:///:memory:', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

vocabulary = Vocabulary(name='Vocabulary')
tag1 = Tag(vocabulary=vocabulary, name='Tag1')
tag2 = Tag(vocabulary=vocabulary, name='Tag2')

vocabulary2 = Vocabulary(name='Vocabulary2')
tag3 = Tag(vocabulary=vocabulary2, name='Tag3')

entity = Entity()

session.add(vocabulary)
session.add(vocabulary2)
session.add(entity)
session.commit()

TagAssociation(entity=entity, tag=tag1)  # first tag
session.commit()
TagAssociation(entity=entity, tag=tag3)  # tag from a different vocabulary, no 
problem
session.commit()
try:
    TagAssociation(entity=entity, tag=tag2)  # a second tag from the same 
vocabulary
    session.commit()
except ValueError:
    # this is expected
    session.rollback()
entity.tags = [TagAssociation(entity=entity, tag=tag2)]
session.commit()  # here no exception is expected


Which event or place and method are best to solve that? With „inspect“ I 
couldn’t find anything that would help me. 

> you'd need to show me how to illustrate the .tags collection storing two 
> TagAssociation objects when you've explicitly set the collection to store a 
> single TagAssociation
I don’t get it, where do I set that?


> Am 05.05.2016 um 23:26 schrieb Mike Bayer <[email protected]>:
> 
> 
> 
> On 05/03/2016 06:28 PM, Tim-Christian Mundt wrote:
>> I have entities which I’d like to tag with tags from a vocabulary. The 
>> associations between entities and tags are implemented with „manual M:N“, 
>> because they carry more information.
>> 
>> class Entity(Base):
>>     id = Column(Integer, primary_key=True)
>>     tags = relationship('TagAssociation', backref='entity',
>>                         cascade="all, delete, delete-orphan")
>> 
>> class Vocabulary(Base):
>>     id = Column(Integer, primary_key=True)
>>     name = Column(String(255), nullable=False, unique=True)
>> 
>> class Tag(Base):
>>     id = Column(Integer, primary_key=True)
>>     vocabularyId = Column(Integer, ForeignKey(Vocabulary.id), nullable=False)
>>     vocabulary = relationship(Vocabulary, lazy='joined',
>>                               backref=backref('tags', cascade='all, 
>> delete-orphan'))
>>     name = Column(String(50), nullable=False)
>> 
>> class TagAssociation(Base):
>>     entityId = Column(Integer, ForeignKey(Entity.id), primary_key=True)
>>     tagId = Column(Integer, ForeignKey(Tag.id), primary_key=True)
>>     tag = relationship(Tag, lazy='joined‘)
>> 
>> 
>> Now I want to make sure that I don’t have more than one tag from a single 
>> vocabulary on an entity. Currently I’m using
>> the before_insert event on TagAssociation which works well except in this 
>> case:
>> 
>> entity = Entity()
>> entity.tags.append(TagAssociation(entity=entity, tag=some_tag))
>> session.add(entity)
>> session.flush()
>> entity.tags = [TagAssociation(entity=entity, 
>> tag=some_other_tag_from_same_vocabulary)]
>> session.flush()
>> 
>> When the validation function is called during the second flush, the 
>> collection (assoc_to_be_inserted.entity.tags)
>> has both tags in it (which I don’t understand).
> 
> This is not enough detail for me to reproduce that, you'd need to show me how 
> to illustrate the .tags collection storing two TagAssociation objects when 
> you've explicitly set the collection to store a single TagAssociation.   
> That's not anything that reproduces here.
> 
> 
> 
> I couldn’t find a way to detect if one of them is going to be deleted.
> 
> if a collection is assigned to a specific value, then the items that are no 
> longer present in that collection are added to a list called "deleted".  That 
> can be seen like this:
> 
> from sqlalchemy import inspect
> 
> inspect(my_entity).attrs.tags.history
> 
> 
>> Hence, the validation fails, although the final result would be correct.
>> 
>> Is there any event I can use to reliably implement that validation?
> 
> There's not enough detail here for me to know exactly how you're doing this 
> validation and under what scenario you're seeing something unexpected.   The 
> best format is to send an MCVE (http://stackoverflow.com/help/mcve).
> 
> 
> 
> 
> 
> 
>> 
>> Thanks,
>> Tim
>> 
> 
> -- 
> 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 post to this group, send email to [email protected].
> Visit this group at https://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.

-- 
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 post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to