Guest User

Untitled

a guest
Apr 14th, 2025
48
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.95 KB | None | 0 0
  1. I am using SQLAlchemy with sqlite, python 3.13.3. In case it matters, this is for a Discord bot, but I doubt that the code will change for that.
  2.  
  3. I have the following tables:
  4.  
  5. [CODE lang="python" title="DB tables"]import sqlalchemy as sqal
  6.  
  7. class Base(DeclarativeBase):
  8. pass
  9.  
  10.  
  11. client_group_association = sqal.Table(
  12. "client_group_association",
  13. Base.metadata,
  14. sqal.Column("client_id", sqal.UUID, sqal.ForeignKey("client_info.id"), primary_key=True),
  15. sqal.Column("group_id", sqal.UUID, sqal.ForeignKey("group_info.id"), primary_key=True)
  16.  
  17. )
  18.  
  19.  
  20. class ClientInfo(Base):
  21. """Stores information of clients."""
  22.  
  23. __tablename__ = "client_info"
  24.  
  25. id: Mapped[uuid6.UUID] = mapped_column(sqal.UUID(as_uuid=True), primary_key=True,
  26.  
  27. insert_default=uuid6.uuid7())
  28.  
  29. name: Mapped[str] = mapped_column(sqal.String, nullable=False)
  30. email: Mapped[str] = mapped_column(sqal.String, nullable=False)
  31. phone: Mapped[str] = mapped_column(sqal.String, nullable=False)
  32. age: Mapped[int] = mapped_column(sqal.Integer, nullable=False)
  33.  
  34. # Other unrelated fields omitted
  35.  
  36. groups: Mapped[List[uuid6.UUID]] = mapped_column(UUIDList, nullable=True)
  37.  
  38. group_objs: Mapped[List["GroupInfo"]] = relationship(
  39. secondary=client_group_association,
  40. back_populates="client_objs"
  41. )
  42.  
  43. __table_args__ = (sqal.UniqueConstraint('name', 'email', 'phone', name='_unique_client_details'),
  44. sqal.ForeignKeyConstraint(['groups'], ['group_info.id'],
  45. onupdate='CASCADE', ondelete='CASCADE'))
  46.  
  47.  
  48. class GroupInfo(Base):
  49. """Stores information of groups."""
  50.  
  51. __tablename__ = "group_info"
  52.  
  53.  
  54. id: Mapped[uuid6.UUID] = mapped_column(sqal.UUID(as_uuid=True), primary_key=True,
  55. insert_default=uuid6.uuid7())
  56.  
  57.  
  58. location: Mapped[Locations] = mapped_column(sqal.Enum(Locations), nullable=False)
  59.  
  60. members: Mapped[List[uuid6.UUID]] = mapped_column(UUIDList, nullable=True)
  61.  
  62. client_objs: Mapped[List["ClientInfo"]] = relationship(
  63. secondary=client_group_association,
  64. back_populates="group_objs"
  65. )
  66.  
  67. __table_args__ = (sqal.UniqueConstraint('members', name='_unique_members'),
  68. sqal.ForeignKeyConstraint(['members'], ['client_info.id'],
  69. onupdate='CASCADE', ondelete='CASCADE'))[/CODE]
  70.  
  71.  
  72. My aim was to set up a proper bidirectional many-to-many relationship such that I can access the [ICODE]GroupInfo[/ICODE] objects directly from a [ICODE]ClientInfo[/ICODE] object (which represents the groups the user is in), and the [ICODE]ClientInfo[/ICODE] objects from [ICODE]GroupInfo[/ICODE] (representing the clients in the group).
  73.  
  74.  
  75. I am using an [ICODE]AsyncSession[/ICODE], which I obtain as follows:
  76.  
  77. [CODE=python]__LOCK: asyncio.Lock = asyncio.Lock()
  78. __SQL_ENGINE: AsyncEngine = create_async_engine(f"sqlite+aiosqlite:///{DATABASE_NAME}")
  79.  
  80. @contextlib.asynccontextmanager
  81. async def get_async_session(self, locked: bool = True) -> AsyncIterator[AsyncSession]:
  82.  
  83. Session = sessionmaker(bind=self.__SQL_ENGINE, class_=AsyncSession)
  84. if locked:
  85. async with self.__LOCK:
  86. async with Session() as session:
  87. yield session
  88. else:
  89. async with Session() as session:
  90. yield session[/CODE]
  91.  
  92. Things seemed to work for a while; I could retrieve the nested objects using nested [ICODE]selectinload[/ICODE] statements:
  93. [CODE=python]stmt = select(ClientInfo).where(...)\
  94. .limit(10)\
  95. .options(selectinload(ClientInfo.group_objs)\
  96. .selectinload(GroupInfo.client_objs)) # Force eager-loading[/CODE]
  97.  
  98. But now, every time I want to add a new group, I am facing this:
  99.  
  100. [CODE]sqlalchemy.exc.InvalidRequestError: Can't attach instance <ClientInfo at 0x7baada2f3cb0>; another instance with key (<class 'db_models.ClientInfo'>, (UUID('019634d4-1422-7a0e-9a21-96941acc10dd'),), None) is already present in this session.[/CODE]
  101.  
  102. Note that every time I wish to access the database, I create a new session. The lock is also supposed to allow only one session at a time I guess.
  103.  
  104. The error crops when I add the [ICODE]ClientInfo[/ICODE] objects into the [ICODE]GroupInfo[/ICODE] object. This is the most essential part of the transaction because I refer to the client objects through that relationship in the future. And this is done before adding the [ICODE]GroupInfo[/ICODE] object to the current session and committing.
  105.  
  106. [CODE=python]group_info: GroupInfo = GroupInfo(id=uuid6.uuid7(),
  107. location=self.location,
  108. members=list(self.clients.keys()))
  109.  
  110. for client_info in self.clients.values():
  111. group_info.client_objs.append(client_info)
  112. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^[/CODE]
  113.  
  114. Most probably this happens because I load some [ICODE]ClientInfo[/ICODE] objects from a search query to let the user pick a person. However, this is done in a separate instance of [ICODE]AsyncSession[/ICODE], and I store the data of the chosen individuals in a list as an instance variable, and make sure to expunge and expire all variables from that session after loading the selected person's info into the instance variable.
  115.  
  116.  
  117. I have tried to delete those objects from the current session using
  118.  
  119. [CODE=python]session.expunge(client_info)
  120. session.expire(client_info)[/CODE]
  121.  
  122. I have also created two instance variables in the main class which will hold the [ICODE]ClientInfo.group_objs[/ICODE] field and [ICODE]GroupInfo.client_objs[/ICODE] relationships, and try to use that if adding to the session returns an error:
  123.  
  124. [CODE=python]# In constructor
  125. self.group_objs_relationship = None
  126. self.client_objs_relationship = None
  127.  
  128. # Later in the separate class
  129. bot.client_object_relationship = group_info.client_objs # Nullability checked
  130. bot.group_objs_relationship = group_info.client_objs[0].group_objs # Nullability checked
  131.  
  132. # When adding the new group:
  133. try:
  134. session.add(group_info)
  135.  
  136. except sqal.exc.InvalidRequestError:
  137. print('Error raised when adding GroupInfo, trying alternate path!')
  138.  
  139. if bot.group_objs_relationship:
  140. bot.group_objs_relationship.append(group_info)
  141.  
  142. else:
  143. print('Sorry, could not add that.')
  144.  
  145. finally:
  146. await session.commit()[/CODE]
  147.  
  148. This works for [ICODE]GroupInfo[/ICODE], but I cannot think of an alternative to this for adding the [ICODE]ClientInfo[/ICODE] objects to the [ICODE]GroupInfo[/ICODE] object before committing, because none of the expunge statements seem to work in that case.
  149.  
  150. Any assistance is appreciated.
  151.  
  152. [i][size=2]N.B.: For full transparency: I have posted this question on SO too, but the chances of getting a reply there is bleak, to be honest. Hence, posting here too because we have good programmers here.[/size][/i]
Advertisement
Add Comment
Please, Sign In to add comment