Skip to content

Commit 41c30cc

Browse files
CaselITzzzeek
authored andcommitted
Added merge_all and delete_all
Added the utility method :meth:`_orm.Session.merge_all` and :meth:`_orm.Session.delete_all` that operate on a collection of instances. Fixes: #11776 Change-Id: Ifd70ba2850db7c5e7aee482799fd65c348c2899a
1 parent e4f0afe commit 41c30cc

9 files changed

Lines changed: 241 additions & 31 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.. change::
2+
:tags: orm, usecase
3+
:tickets: 11776
4+
5+
Added the utility method :meth:`_orm.Session.merge_all` and
6+
:meth:`_orm.Session.delete_all` that operate on a collection
7+
of instances.

lib/sqlalchemy/ext/asyncio/scoping.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"commit",
8686
"connection",
8787
"delete",
88+
"delete_all",
8889
"execute",
8990
"expire",
9091
"expire_all",
@@ -95,6 +96,7 @@
9596
"is_modified",
9697
"invalidate",
9798
"merge",
99+
"merge_all",
98100
"refresh",
99101
"rollback",
100102
"scalar",
@@ -287,7 +289,7 @@ async def aclose(self) -> None:
287289

288290
return await self._proxied.aclose()
289291

290-
def add(self, instance: object, _warn: bool = True) -> None:
292+
def add(self, instance: object, *, _warn: bool = True) -> None:
291293
r"""Place an object into this :class:`_orm.Session`.
292294
293295
.. container:: class_bases
@@ -530,6 +532,23 @@ async def delete(self, instance: object) -> None:
530532

531533
return await self._proxied.delete(instance)
532534

535+
async def delete_all(self, instances: Iterable[object]) -> None:
536+
r"""Calls :meth:`.AsyncSession.delete` on multiple instances.
537+
538+
.. container:: class_bases
539+
540+
Proxied for the :class:`_asyncio.AsyncSession` class on
541+
behalf of the :class:`_asyncio.scoping.async_scoped_session` class.
542+
543+
.. seealso::
544+
545+
:meth:`_orm.Session.delete_all` - main documentation for delete_all
546+
547+
548+
""" # noqa: E501
549+
550+
return await self._proxied.delete_all(instances)
551+
533552
@overload
534553
async def execute(
535554
self,
@@ -958,6 +977,31 @@ async def merge(
958977

959978
return await self._proxied.merge(instance, load=load, options=options)
960979

980+
async def merge_all(
981+
self,
982+
instances: Iterable[_O],
983+
*,
984+
load: bool = True,
985+
options: Optional[Sequence[ORMOption]] = None,
986+
) -> Sequence[_O]:
987+
r"""Calls :meth:`.AsyncSession.merge` on multiple instances.
988+
989+
.. container:: class_bases
990+
991+
Proxied for the :class:`_asyncio.AsyncSession` class on
992+
behalf of the :class:`_asyncio.scoping.async_scoped_session` class.
993+
994+
.. seealso::
995+
996+
:meth:`_orm.Session.merge_all` - main documentation for merge_all
997+
998+
999+
""" # noqa: E501
1000+
1001+
return await self._proxied.merge_all(
1002+
instances, load=load, options=options
1003+
)
1004+
9611005
async def refresh(
9621006
self,
9631007
instance: object,

lib/sqlalchemy/ext/asyncio/session.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,16 @@ async def delete(self, instance: object) -> None:
775775
"""
776776
await greenlet_spawn(self.sync_session.delete, instance)
777777

778+
async def delete_all(self, instances: Iterable[object]) -> None:
779+
"""Calls :meth:`.AsyncSession.delete` on multiple instances.
780+
781+
.. seealso::
782+
783+
:meth:`_orm.Session.delete_all` - main documentation for delete_all
784+
785+
"""
786+
await greenlet_spawn(self.sync_session.delete_all, instances)
787+
778788
async def merge(
779789
self,
780790
instance: _O,
@@ -794,6 +804,24 @@ async def merge(
794804
self.sync_session.merge, instance, load=load, options=options
795805
)
796806

807+
async def merge_all(
808+
self,
809+
instances: Iterable[_O],
810+
*,
811+
load: bool = True,
812+
options: Optional[Sequence[ORMOption]] = None,
813+
) -> Sequence[_O]:
814+
"""Calls :meth:`.AsyncSession.merge` on multiple instances.
815+
816+
.. seealso::
817+
818+
:meth:`_orm.Session.merge_all` - main documentation for merge_all
819+
820+
"""
821+
return await greenlet_spawn(
822+
self.sync_session.merge_all, instances, load=load, options=options
823+
)
824+
797825
async def flush(self, objects: Optional[Sequence[Any]] = None) -> None:
798826
"""Flush all the object changes to the database.
799827
@@ -1122,7 +1150,7 @@ def __iter__(self) -> Iterator[object]:
11221150

11231151
return self._proxied.__iter__()
11241152

1125-
def add(self, instance: object, _warn: bool = True) -> None:
1153+
def add(self, instance: object, *, _warn: bool = True) -> None:
11261154
r"""Place an object into this :class:`_orm.Session`.
11271155
11281156
.. container:: class_bases

lib/sqlalchemy/orm/loading.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,7 @@ def merge_frozen_result(session, statement, frozen_result, load=True):
327327
statement, legacy=False
328328
)
329329

330-
autoflush = session.autoflush
331-
try:
332-
session.autoflush = False
330+
with session.no_autoflush:
333331
mapped_entities = [
334332
i
335333
for i, e in enumerate(ctx._entities)
@@ -356,8 +354,6 @@ def merge_frozen_result(session, statement, frozen_result, load=True):
356354
result.append(keyed_tuple(newrow))
357355

358356
return frozen_result.with_new_rows(result)
359-
finally:
360-
session.autoflush = autoflush
361357

362358

363359
@util.became_legacy_20(

lib/sqlalchemy/orm/scoping.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def __get__(self, instance: Any, owner: Type[_T]) -> Query[_T]: ...
116116
"commit",
117117
"connection",
118118
"delete",
119+
"delete_all",
119120
"execute",
120121
"expire",
121122
"expire_all",
@@ -130,6 +131,7 @@ def __get__(self, instance: Any, owner: Type[_T]) -> Query[_T]: ...
130131
"bulk_insert_mappings",
131132
"bulk_update_mappings",
132133
"merge",
134+
"merge_all",
133135
"query",
134136
"refresh",
135137
"rollback",
@@ -350,7 +352,7 @@ def __iter__(self) -> Iterator[object]:
350352

351353
return self._proxied.__iter__()
352354

353-
def add(self, instance: object, _warn: bool = True) -> None:
355+
def add(self, instance: object, *, _warn: bool = True) -> None:
354356
r"""Place an object into this :class:`_orm.Session`.
355357
356358
.. container:: class_bases
@@ -673,11 +675,32 @@ def delete(self, instance: object) -> None:
673675
674676
:ref:`session_deleting` - at :ref:`session_basics`
675677
678+
:meth:`.Session.delete_all` - multiple instance version
679+
676680
677681
""" # noqa: E501
678682

679683
return self._proxied.delete(instance)
680684

685+
def delete_all(self, instances: Iterable[object]) -> None:
686+
r"""Calls :meth:`.Session.delete` on multiple instances.
687+
688+
.. container:: class_bases
689+
690+
Proxied for the :class:`_orm.Session` class on
691+
behalf of the :class:`_orm.scoping.scoped_session` class.
692+
693+
.. seealso::
694+
695+
:meth:`.Session.delete` - main documentation on delete
696+
697+
.. versionadded: 2.1
698+
699+
700+
""" # noqa: E501
701+
702+
return self._proxied.delete_all(instances)
703+
681704
@overload
682705
def execute(
683706
self,
@@ -1567,11 +1590,38 @@ def merge(
15671590
:func:`.make_transient_to_detached` - provides for an alternative
15681591
means of "merging" a single object into the :class:`.Session`
15691592
1593+
:meth:`.Session.merge_all` - multiple instance version
1594+
15701595
15711596
""" # noqa: E501
15721597

15731598
return self._proxied.merge(instance, load=load, options=options)
15741599

1600+
def merge_all(
1601+
self,
1602+
instances: Iterable[_O],
1603+
*,
1604+
load: bool = True,
1605+
options: Optional[Sequence[ORMOption]] = None,
1606+
) -> Sequence[_O]:
1607+
r"""Calls :meth:`.Session.merge` on multiple instances.
1608+
1609+
.. container:: class_bases
1610+
1611+
Proxied for the :class:`_orm.Session` class on
1612+
behalf of the :class:`_orm.scoping.scoped_session` class.
1613+
1614+
.. seealso::
1615+
1616+
:meth:`.Session.merge` - main documentation on merge
1617+
1618+
.. versionadded: 2.1
1619+
1620+
1621+
""" # noqa: E501
1622+
1623+
return self._proxied.merge_all(instances, load=load, options=options)
1624+
15751625
@overload
15761626
def query(self, _entity: _EntityType[_O]) -> Query[_O]: ...
15771627

lib/sqlalchemy/orm/session.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3459,7 +3459,7 @@ def _remove_newly_deleted(
34593459
if persistent_to_deleted is not None:
34603460
persistent_to_deleted(self, state)
34613461

3462-
def add(self, instance: object, _warn: bool = True) -> None:
3462+
def add(self, instance: object, *, _warn: bool = True) -> None:
34633463
"""Place an object into this :class:`_orm.Session`.
34643464
34653465
Objects that are in the :term:`transient` state when passed to the
@@ -3544,16 +3544,30 @@ def delete(self, instance: object) -> None:
35443544
35453545
:ref:`session_deleting` - at :ref:`session_basics`
35463546
3547+
:meth:`.Session.delete_all` - multiple instance version
3548+
35473549
"""
35483550
if self._warn_on_events:
35493551
self._flush_warning("Session.delete()")
35503552

3551-
try:
3552-
state = attributes.instance_state(instance)
3553-
except exc.NO_STATE as err:
3554-
raise exc.UnmappedInstanceError(instance) from err
3553+
self._delete_impl(object_state(instance), instance, head=True)
3554+
3555+
def delete_all(self, instances: Iterable[object]) -> None:
3556+
"""Calls :meth:`.Session.delete` on multiple instances.
35553557
3556-
self._delete_impl(state, instance, head=True)
3558+
.. seealso::
3559+
3560+
:meth:`.Session.delete` - main documentation on delete
3561+
3562+
.. versionadded: 2.1
3563+
3564+
"""
3565+
3566+
if self._warn_on_events:
3567+
self._flush_warning("Session.delete_all()")
3568+
3569+
for instance in instances:
3570+
self._delete_impl(object_state(instance), instance, head=True)
35573571

35583572
def _delete_impl(
35593573
self, state: InstanceState[Any], obj: object, head: bool
@@ -3955,32 +3969,62 @@ def merge(
39553969
:func:`.make_transient_to_detached` - provides for an alternative
39563970
means of "merging" a single object into the :class:`.Session`
39573971
3972+
:meth:`.Session.merge_all` - multiple instance version
3973+
39583974
"""
39593975

39603976
if self._warn_on_events:
39613977
self._flush_warning("Session.merge()")
39623978

3963-
_recursive: Dict[InstanceState[Any], object] = {}
3964-
_resolve_conflict_map: Dict[_IdentityKeyType[Any], object] = {}
3965-
39663979
if load:
39673980
# flush current contents if we expect to load data
39683981
self._autoflush()
39693982

3970-
object_mapper(instance) # verify mapped
3971-
autoflush = self.autoflush
3972-
try:
3973-
self.autoflush = False
3983+
with self.no_autoflush:
39743984
return self._merge(
3975-
attributes.instance_state(instance),
3985+
object_state(instance),
39763986
attributes.instance_dict(instance),
39773987
load=load,
39783988
options=options,
3979-
_recursive=_recursive,
3980-
_resolve_conflict_map=_resolve_conflict_map,
3989+
_recursive={},
3990+
_resolve_conflict_map={},
39813991
)
3982-
finally:
3983-
self.autoflush = autoflush
3992+
3993+
def merge_all(
3994+
self,
3995+
instances: Iterable[_O],
3996+
*,
3997+
load: bool = True,
3998+
options: Optional[Sequence[ORMOption]] = None,
3999+
) -> Sequence[_O]:
4000+
"""Calls :meth:`.Session.merge` on multiple instances.
4001+
4002+
.. seealso::
4003+
4004+
:meth:`.Session.merge` - main documentation on merge
4005+
4006+
.. versionadded: 2.1
4007+
4008+
"""
4009+
4010+
if self._warn_on_events:
4011+
self._flush_warning("Session.merge_all()")
4012+
4013+
if load:
4014+
# flush current contents if we expect to load data
4015+
self._autoflush()
4016+
4017+
return [
4018+
self._merge(
4019+
object_state(instance),
4020+
attributes.instance_dict(instance),
4021+
load=load,
4022+
options=options,
4023+
_recursive={},
4024+
_resolve_conflict_map={},
4025+
)
4026+
for instance in instances
4027+
]
39844028

39854029
def _merge(
39864030
self,

0 commit comments

Comments
 (0)