Skip to content

Commit 3f8bc66

Browse files
committed
Transactions now support models routed to different databases.
1 parent fa74336 commit 3f8bc66

File tree

4 files changed

+81
-42
lines changed

4 files changed

+81
-42
lines changed

CHANGELOG.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,39 @@
22

33
## [Unreleased]
44

5+
## [3.0.1] - 2020-08-05
6+
7+
### Updated
8+
9+
- Transactions now support models routed to different databases.
10+
511
## [3.0.0] - 2020-07-28
12+
613
### Changed
7-
- **Potentially breaking change** Retain PK if provided and ensure a crash if it mismatches on key_field. #10
814

9-
If your input models have the PK set already and expect it to be cleared by bulk_sync, that won't happen anymore.
10-
Please clear the pks yourself if you want them to seem like "new" objects.
15+
- **Potentially breaking change** Retain PK if provided and ensure a crash if it mismatches on key_field. #10
16+
17+
If your input models have the PK set already and expect it to be cleared by bulk_sync, that won't happen anymore.
18+
Please clear the pks yourself if you want them to seem like "new" objects.
1119

1220
## [2.1.0] - 2020-06-05
21+
1322
### Added
14-
- Added support for skip_deletes, skip_creates, and skip_updates in bulk_sync method. #9, pull request from [@mikefreemanwd](https://github.com/mikefreemanwd).
23+
24+
- Added support for skip_deletes, skip_creates, and skip_updates in bulk_sync method. #9, pull request from [@mikefreemanwd](https://github.com/mikefreemanwd).
1525

1626
### Updated
17-
- Updated stats returned by bulk_sync to reflect what actually happened given the flags rather than what would have happend if all flags are false.
27+
28+
- Updated stats returned by bulk_sync to reflect what actually happened given the flags rather than what would have happend if all flags are false.
1829

1930
## [2.0.0] - 2020-05-19
31+
2032
### Removed
21-
- Removed support for Django versions before 2.2. Please use 1.x series for Django < 2.2.
2233

34+
- Removed support for Django versions before 2.2. Please use 1.x series for Django < 2.2.
2335

24-
[Unreleased]: https://github.com/mathandpencil/django-bulk-sync/compare/v3.0.0..HEAD
36+
[unreleased]: https://github.com/mathandpencil/django-bulk-sync/compare/v3.0.0..HEAD
37+
[3.0.1]: https://github.com/mathandpencil/django-bulk-sync/compare/v3.0.0..v3.0.1
2538
[3.0.0]: https://github.com/mathandpencil/django-bulk-sync/compare/v2.1.0..v3.0.0
2639
[2.1.0]: https://github.com/mathandpencil/django-bulk-sync/compare/v2.0.0..v2.1.0
27-
[2.0.0]: https://github.com/mathandpencil/django-bulk-sync/releases/tag/v2.0.0
40+
[2.0.0]: https://github.com/mathandpencil/django-bulk-sync/releases/tag/v2.0.0

README.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Combine bulk create, update, and delete into a single call.
66

77
It manages all necessary creates, updates, and deletes with as few database calls as possible to maximize performance.
88

9+
It can use either database PKs or `key_fields` to match up objects with existing records.
10+
911
## Installation
1012

1113
The package is available on pip as [django-bulk-sync][django-bulk-sync]. Run:
@@ -43,10 +45,12 @@ for line in company_import_file:
4345
new_models.append(e)
4446

4547
# `filters` controls the subset of objects considered when deciding to
46-
# update or delete.
48+
# update or delete. Here we sync only company 501 employees.
4749
filters = Q(company_id=501)
50+
4851
# `key_fields` matches an existing object if all `key_fields` are equal.
4952
key_fields = ('name', )
53+
5054
ret = bulk_sync(
5155
new_models=new_models,
5256
filters=filters,
@@ -62,27 +66,28 @@ Under the hood, it will atomically call `bulk_create`, `bulk_update`, and a sing
6266

6367
## Argument Reference
6468

65-
`def bulk_sync(new_models, key_fields, filters, batch_size=None, fields=None):`
69+
`def bulk_sync(new_models, key_fields, filters, batch_size=None, fields=None, skip_creates=False, skip_updates=False, skip_deletes=False):`
6670
Combine bulk create, update, and delete. Make the DB match a set of in-memory objects.
6771

6872
- `new_models`: An iterable of Django ORM `Model` objects that you want stored in the database. They may or may not have `id` set, but you should not have already called `save()` on them.
69-
- `key_fields`: Identifying attribute name(s) to match up `new_models` items with database rows. If a foreign key is being used as a key field, be sure to pass the `fieldname_id` rather than the `fieldname`.
73+
- `key_fields`: Identifying attribute name(s) to match up `new_models` items with database rows. If a foreign key is being used as a key field, be sure to pass the `fieldname_id` rather than the `fieldname`. Use `['pk']` if you know the PKs already and want to use them to identify and match up `new_models` with existing database rows.
7074
- `filters`: Q() filters specifying the subset of the database to work in. Use `None` or `[]` if you want to sync against the entire table.
71-
- `batch_size`: passes through to Django `bulk_create.batch_size` and `bulk_update.batch_size`, and controls how many objects are created/updated per SQL query.
75+
- `batch_size`: (optional) passes through to Django `bulk_create.batch_size` and `bulk_update.batch_size`, and controls how many objects are created/updated per SQL query.
7276
- `fields`: (optional) List of fields to update. If not set, will sync all fields that are editable and not auto-created.
73-
- `skip_creates`: If truthy, will not perform any object creations needed to fully sync. Defaults to not skip.
74-
- `skip_updates`: If truthy, will not perform any object updates needed to fully sync. Defaults to not skip.
75-
- `skip_deletes`: If truthy, will not perform any object deletions needed to fully sync. Defaults to not skip.
77+
- `skip_creates`: (optional) If truthy, will not perform any object creations needed to fully sync. Defaults to not skip.
78+
- `skip_updates`: (optional) If truthy, will not perform any object updates needed to fully sync. Defaults to not skip.
79+
- `skip_deletes`: (optional) If truthy, will not perform any object deletions needed to fully sync. Defaults to not skip.
7680

7781
- Returns a dict:
78-
82+
```
7983
{
8084
'stats': {
81-
"created": number of `new_models` not found in database and so created,
82-
"updated": number of `new_models` that were found in database as matched by `key_fields`,
83-
"deleted": number of deleted objects - rows in database that matched `filters` but were not present in `new_models`.
84-
}
85+
"created": number of `new_models` not found in database and so created,
86+
"updated": number of `new_models` that were found in database as matched by `key_fields`,
87+
"deleted": number of deleted objects - rows in database that matched `filters` but were not present in `new_models`.
88+
}
8589
}
90+
```
8691
8792
`def bulk_compare(old_models, new_models, key_fields, ignore_fields=None):`
8893
Compare two sets of models by `key_fields`.
@@ -92,7 +97,16 @@ Compare two sets of models by `key_fields`.
9297
- `key_fields`: Identifying attribute name(s) to match up `new_models` items with database rows. If a foreign key
9398
is being used as a key field, be sure to pass the `fieldname_id` rather than the `fieldname`.
9499
- `ignore_fields`: (optional) If set, provide field names that should not be considered when comparing objects.
95-
- Returns dict of: `{ 'added': list of all added objects. 'unchanged': list of all unchanged objects. 'updated': list of all updated objects. 'updated_details': dict of {obj: {field_name: (old_value, new_value)}} for all changed fields in each updated object. 'removed': list of all removed objects. }`
100+
- Returns dict:
101+
```
102+
{
103+
'added': list of all added objects.
104+
'unchanged': list of all unchanged objects.
105+
'updated': list of all updated objects.
106+
'updated_details': dict of {obj: {field_name: (old_value, new_value)}} for all changed fields in each updated object.
107+
'removed': list of all removed objects.
108+
}
109+
```
96110
97111
## Frameworks Supported
98112

bulk_sync/__init__.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,45 @@
11
from collections import OrderedDict
22
import logging
3-
from django.db import transaction
3+
from django.db import transaction, router
44

55
logger = logging.getLogger(__name__)
66

77

8-
def bulk_sync(new_models, key_fields, filters, batch_size=None, fields=None, skip_creates=False, skip_updates=False, skip_deletes=False):
8+
def bulk_sync(
9+
new_models,
10+
key_fields,
11+
filters,
12+
batch_size=None,
13+
fields=None,
14+
skip_creates=False,
15+
skip_updates=False,
16+
skip_deletes=False,
17+
):
918
""" Combine bulk create, update, and delete. Make the DB match a set of in-memory objects.
1019
1120
`new_models`: Django ORM objects that are the desired state. They may or may not have `id` set.
1221
`key_fields`: Identifying attribute name(s) to match up `new_models` items with database rows. If a foreign key
1322
is being used as a key field, be sure to pass the `fieldname_id` rather than the `fieldname`.
1423
`filters`: Q() filters specifying the subset of the database to work in. Use `None` or `[]` if you want to sync against the entire table.
15-
`batch_size`: passes through to Django `bulk_create.batch_size` and `bulk_update.batch_size`, and controls
24+
`batch_size`: (optional) passes through to Django `bulk_create.batch_size` and `bulk_update.batch_size`, and controls
1625
how many objects are created/updated per SQL query.
1726
`fields`: (optional) list of fields to update. If not set, will sync all fields that are editable and not auto-created.
1827
`skip_creates`: If truthy, will not perform any object creations needed to fully sync. Defaults to not skip.
19-
`skip_updates`: If truthy, will not perform any object updates needed to fully sync. Defaults to not skip.
20-
`skip_deletes`: If truthy, will not perform any object deletions needed to fully sync. Defaults to not skip.
28+
`skip_updates`: If truthy, will not perform any object updates needed to fully sync. Defaults to not skip.
29+
`skip_deletes`: If truthy, will not perform any object deletions needed to fully sync. Defaults to not skip.
2130
"""
2231
db_class = new_models[0].__class__
2332

2433
if fields is None:
2534
# Get a list of fields that aren't PKs and aren't editable (e.g. auto_add_now) for bulk_update
26-
fields = [field.name
27-
for field in db_class._meta.fields
28-
if not field.primary_key and not field.auto_created and field.editable]
29-
30-
with transaction.atomic():
35+
fields = [
36+
field.name
37+
for field in db_class._meta.fields
38+
if not field.primary_key and not field.auto_created and field.editable
39+
]
40+
41+
using = router.db_for_write(db_class)
42+
with transaction.atomic(using=using):
3143
objs = db_class.objects.all()
3244
if filters:
3345
objs = objs.filter(filters)
@@ -51,7 +63,7 @@ def get_key(obj):
5163

5264
if not skip_creates:
5365
db_class.objects.bulk_create(new_objs, batch_size=batch_size)
54-
66+
5567
if not skip_updates:
5668
db_class.objects.bulk_update(existing_objs, fields=fields, batch_size=batch_size)
5769

@@ -62,9 +74,9 @@ def get_key(obj):
6274
assert len(existing_objs) == len(new_models) - len(new_objs)
6375

6476
stats = {
65-
"created": 0 if skip_creates else len(new_objs),
66-
"updated": 0 if skip_updates else (len(new_models) - len(new_objs)),
67-
"deleted": 0 if skip_deletes else len(obj_dict)
77+
"created": 0 if skip_creates else len(new_objs),
78+
"updated": 0 if skip_updates else (len(new_models) - len(new_objs)),
79+
"deleted": 0 if skip_deletes else len(obj_dict),
6880
}
6981

7082
logger.debug(

setup.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@
55

66
setuptools.setup(
77
name="django-bulk-sync",
8-
version='3.0.0',
8+
version="3.0.1",
99
description="Combine bulk add, update, and delete into a single call.",
1010
long_description=long_description,
11-
long_description_content_type='text/markdown',
11+
long_description_content_type="text/markdown",
1212
url="https://github.com/mathandpencil/django-bulk-sync",
1313
author="Scott Stafford",
1414
author_email="[email protected]",
1515
packages=setuptools.find_packages(),
1616
classifiers=[
17-
'Programming Language :: Python :: 3',
18-
'License :: OSI Approved :: MIT License',
19-
'Operating System :: OS Independent',
20-
'Framework :: Django',
21-
'Framework :: Django :: 2.2',
17+
"Programming Language :: Python :: 3",
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: OS Independent",
20+
"Framework :: Django",
21+
"Framework :: Django :: 2.2",
2222
],
2323
zip_safe=False,
2424
)

0 commit comments

Comments
 (0)