@@ -208,6 +208,71 @@ Delete Where
208208 return await repository.delete_where(Post.published.is_(False ))
209209
210210
211+ Composite Primary Keys
212+ ----------------------
213+
214+ Advanced Alchemy supports models with composite primary keys. For these models, the ``PrimaryKeyType ``
215+ accepts several formats when identifying records:
216+
217+ **Tuple Format ** - Pass primary key values as a tuple in column definition order:
218+
219+ .. code-block :: python
220+
221+ from advanced_alchemy.repository import SQLAlchemyAsyncRepository
222+ from sqlalchemy import Column, ForeignKey, Integer, String
223+ from sqlalchemy.orm import DeclarativeBase
224+
225+ class Base (DeclarativeBase ):
226+ pass
227+
228+ class UserRole (Base ):
229+ """ Model with composite primary key."""
230+ __tablename__ = " user_role"
231+ user_id: int = Column(Integer, ForeignKey(" user.id" ), primary_key = True )
232+ role_id: int = Column(Integer, ForeignKey(" role.id" ), primary_key = True )
233+ permissions: str = Column(String, nullable = True )
234+
235+ class UserRoleRepository (SQLAlchemyAsyncRepository[UserRole]):
236+ model_type = UserRole
237+
238+ async def get_user_role (db_session , user_id : int , role_id : int ) -> UserRole:
239+ repository = UserRoleRepository(session = db_session)
240+ # Tuple format: values in column definition order
241+ return await repository.get((user_id, role_id))
242+
243+ **Dict Format ** - Pass primary key values as a dictionary with column names as keys:
244+
245+ .. code-block :: python
246+
247+ async def get_user_role_by_dict (db_session , user_id : int , role_id : int ) -> UserRole:
248+ repository = UserRoleRepository(session = db_session)
249+ # Dict format: explicit column names
250+ return await repository.get({" user_id" : user_id, " role_id" : role_id})
251+
252+ **Bulk Operations with Composite Keys ** - Use sequences of tuples or dicts:
253+
254+ .. code-block :: python
255+
256+ async def delete_user_roles (db_session , role_assignments : list[tuple[int , int ]]) -> None :
257+ repository = UserRoleRepository(session = db_session)
258+ # Delete multiple records by their composite keys
259+ await repository.delete_many(role_assignments)
260+
261+ async def get_multiple_user_roles (db_session ) -> list[UserRole]:
262+ repository = UserRoleRepository(session = db_session)
263+ # Get multiple records using dict format
264+ return await repository.get_many([
265+ {" user_id" : 1 , " role_id" : 5 },
266+ {" user_id" : 1 , " role_id" : 6 },
267+ {" user_id" : 2 , " role_id" : 5 },
268+ ])
269+
270+ .. note ::
271+
272+ When using tuple format, ensure values are in the same order as the primary key columns
273+ are defined on the model. Dict format is more explicit and avoids ordering issues.
274+
275+
211276Transaction Management
212277----------------------
213278
0 commit comments