环境
Django==2.0.4
django-import-export==1.0.1
需求:
- 导入需要修改主键,由另外一个字段来决定唯一性(替代主键);
- 一定要先预览再修改,即:不点【确认导入】就不修改数据库;
解决方案
需求1
使用import_id_fields
配置即可,把这个设置为另外一个字段:
class TestModifyProductResource(resources.ModelResource):
"""
以 test_field 为唯一值的导入。p_id 为数据库主键
"""
class Meta:
model = models.TestProduct
skip_unchanged = True
import_id_fields = ('test_field',)
fields = ('p_id', 'field_2', 'field_3', 'field_4', 'test_field')
use_transactions = False
instance_loader_class = TestModifyProductInstanceLoader
def import_row(self, row, *args, **kwargs):
row['dry_run'] = kwargs.get('dry_run')
return super().import_row(row, *args, **kwargs)
def save_instance(self, instance, using_transactions=True, dry_run=False):
if dry_run:
# 如果是导入第一阶段,就不存储。
return
super().save_instance(instance, using_transactions, dry_run)
问题1:
- 在mysql中,因为主键无法修改,所以在导入时必须先新增(新数据)后删除(老数据)。解决办法是自定义
InstanceLoader
,代码如下:
class TestModifyProductInstanceLoader(ModelInstanceLoader):
"""
导入会出现更新主键的情况,如果已经存在相同的主键就需要先增加再删除。
"""
def get_instance(self, row):
# dry_run表示导入阶段,因为不同的阶段行为不一样所以需要把这个信息传递过来。
dry_run = row.pop('dry_run', True)
qs = self.get_queryset().filter(test_field=row['test_field'])
if qs.count() <= 1:
if dry_run:
return qs.first()
instance = super().get_instance(row)
# 删除p_id和test_field一样的记录,不然后续程序会报错。
qs.exclude(p_id=row['p_id']).delete()
return instance
else:
# 下面备份p_id是为了提示正确:能够正确的显示更新数量。
# 如果不需要提示正确,直接对tqs执行delete就行。
zqs = qs.exclude(p_id=row['p_id'])
old_p_id = zqs.first().p_id
zqs.delete()
instance = super().get_instance(row)
instance.p_id = old_p_id
return instance
需求2
按照官方的说法,只需要设置use_transactions = False
即可,实际上不行,因官方实现数据缓存使用了事务来实现的。源码如下:
def import_data(self, dataset, dry_run=False, raise_errors=False,
use_transactions=None, collect_failed_rows=False, **kwargs):
#....
if use_transactions is None:
use_transactions = self.get_use_transactions()
connection = connections[DEFAULT_DB_ALIAS]
supports_transactions = getattr(connection.features, "supports_transactions", False)
if use_transactions and not supports_transactions:
raise ImproperlyConfigured
using_transactions = (use_transactions or dry_run) and supports_transactions
#....
我重写save_instance
函数,强制不存储即可:
def save_instance(self, instance, using_transactions=True, dry_run=False):
if dry_run:
# 如果是导入第一阶段,就不存储。
return
super().save_instance(instance, using_transactions, dry_run)
备注: Django是根据主键是否存在来判定是使用insert还是update的,使用sql是可以修改主键的。