Tải dữ liệu ban đầu với Django 1.7 và di chuyển dữ liệu


95

Gần đây tôi đã chuyển từ Django 1.6 sang 1.7 và tôi đã bắt đầu sử dụng di chuyển (tôi chưa bao giờ sử dụng South).

Trước 1.7, tôi thường tải dữ liệu ban đầu bằng một fixture/initial_data.jsontệp, tệp này được tải bằng python manage.py syncdblệnh (khi tạo cơ sở dữ liệu).

Bây giờ, tôi đã bắt đầu sử dụng di chuyển và hành vi này không được dùng nữa:

Nếu một ứng dụng sử dụng di chuyển, sẽ không có quá trình tải tự động các đồ đạc. Vì các ứng dụng trong Django 2.0 sẽ phải di chuyển nên hành vi này được coi là không được dùng nữa. Nếu bạn muốn tải dữ liệu ban đầu cho một ứng dụng, hãy cân nhắc thực hiện việc đó trong quá trình di chuyển dữ liệu. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

Các tài liệu chính thức không có một ví dụ rõ ràng về cách để làm điều đó, vì vậy câu hỏi của tôi là:

Cách tốt nhất để nhập dữ liệu ban đầu như vậy bằng cách sử dụng di chuyển dữ liệu là gì:

  1. Viết mã Python với nhiều lệnh gọi tới mymodel.create(...),
  2. Sử dụng hoặc viết một hàm Django ( như gọiloaddata ) để tải dữ liệu từ tệp cố định JSON.

Tôi thích lựa chọn thứ hai hơn.

Tôi không muốn sử dụng South, vì Django dường như có thể làm điều đó ngay từ đầu.


3
Ngoài ra, tôi muốn thêm một câu hỏi khác vào câu hỏi ban đầu của OP: Chúng ta nên thực hiện di chuyển dữ liệu như thế nào đối với dữ liệu không thuộc về các ứng dụng của chúng ta. Ví dụ: nếu ai đó đang sử dụng khung công tác trang web, anh ta cần phải có sự cố định với dữ liệu trang web. Vì khuôn khổ các trang web không liên quan đến các ứng dụng của chúng tôi, chúng tôi nên đặt việc di chuyển dữ liệu đó ở đâu? Cảm ơn !
Serafeim

Một điểm quan trọng mà chưa ai giải quyết ở đây là điều gì sẽ xảy ra khi bạn cần thêm dữ liệu được xác định trong quá trình di chuyển dữ liệu vào cơ sở dữ liệu mà bạn đã giả mạo di chuyển trên đó. Vì quá trình di chuyển là giả mạo, quá trình di chuyển dữ liệu của bạn sẽ không chạy và bạn phải thực hiện bằng tay. Tại thời điểm này, bạn cũng có thể chỉ gọi loaddata trên một tệp cố định.
hekevintran

Một tình huống thú vị khác là điều gì sẽ xảy ra nếu bạn di chuyển dữ liệu để tạo các cá thể auth.Group và sau này bạn có một Nhóm mới mà bạn muốn tạo làm dữ liệu gốc. Bạn sẽ cần tạo một lần di chuyển dữ liệu mới. Điều này có thể gây khó chịu vì dữ liệu hạt giống Nhóm của bạn sẽ nằm trong nhiều tệp. Ngoài ra, trong trường hợp bạn muốn đặt lại di chuyển, bạn sẽ phải xem qua để tìm các di chuyển dữ liệu thiết lập dữ liệu gốc và chuyển chúng.
hekevintran

@Serafeim Câu hỏi "Nơi đặt dữ liệu ban đầu cho ứng dụng của bên thứ ba" không thay đổi nếu bạn sử dụng di chuyển dữ liệu thay vì đồ đạc, vì bạn chỉ thay đổi cách tải dữ liệu. Tôi sử dụng một ứng dụng tùy chỉnh nhỏ cho những thứ như thế này. Nếu ứng dụng của bên thứ ba được gọi là "foo", tôi gọi ứng dụng đơn giản của mình chứa di chuyển / vật cố dữ liệu "foo_integration".
guettli

@guettli vâng, có lẽ sử dụng một ứng dụng bổ sung là cách tốt nhất để làm điều đó!
Serafeim

Câu trả lời:


81

Cập nhật : Xem bình luận của @ GwynBleidD bên dưới để biết các vấn đề mà giải pháp này có thể gây ra và xem câu trả lời của @ Rockallite bên dưới để biết cách tiếp cận bền hơn đối với các thay đổi mô hình trong tương lai.


Giả sử bạn có một tệp cố định trong <yourapp>/fixtures/initial_data.json

  1. Tạo di chuyển trống của bạn:

    Trong Django 1.7:

    python manage.py makemigrations --empty <yourapp>

    Trong Django 1.8+, bạn có thể cung cấp tên:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
  2. Chỉnh sửa tệp di chuyển của bạn <yourapp>/migrations/0002_auto_xxx.py

    2.1. Triển khai tùy chỉnh, lấy cảm hứng từ Django ' loaddata(câu trả lời ban đầu):

    import os
    from sys import path
    from django.core import serializers
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
    
        fixture = open(fixture_file, 'rb')
        objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
        for obj in objects:
            obj.save()
        fixture.close()
    
    def unload_fixture(apps, schema_editor):
        "Brutally deleting all entries for this model..."
    
        MyModel = apps.get_model("yourapp", "ModelName")
        MyModel.objects.all().delete()
    
    class Migration(migrations.Migration):  
    
        dependencies = [
            ('yourapp', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(load_fixture, reverse_code=unload_fixture),
        ]

    2.2. Một giải pháp đơn giản hơn cho load_fixture(theo gợi ý của @ juliocesar):

    from django.core.management import call_command
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
        call_command('loaddata', fixture_file) 

    Hữu ích nếu bạn muốn sử dụng một thư mục tùy chỉnh.

    2.3. Đơn giản nhất: gọi loaddatavới app_labelđồ đạc sẽ được tải từ <yourapp>'s fixturesdir tự động:

    from django.core.management import call_command
    
    fixture = 'initial_data'
    
    def load_fixture(apps, schema_editor):
        call_command('loaddata', fixture, app_label='yourapp') 

    Nếu bạn không chỉ định app_label, loaddata sẽ cố gắng tải fixturetên tệp từ tất cả các thư mục đồ đạc ứng dụng (mà bạn có thể không muốn).

  3. Chạy nó

    python manage.py migrate <yourapp>

1
ok, bạn nói đúng ... Cũng gọi loaddata('loaddata', fixture_filename, app_label='<yourapp>')cũng sẽ đi trực tiếp vào ứng dụng vật cố dir (do đó không cần phải xây dựng đường dẫn đầy đủ của trận đấu)
n__o

15
Sử dụng phương pháp đó, bộ tuần tự sẽ hoạt động trên trạng thái mô hình từ models.pycác tệp hiện tại , có thể có một số trường bổ sung hoặc một số thay đổi khác. Nếu một số thay đổi được thực hiện sau khi tạo di chuyển, nó sẽ không thành công (vì vậy chúng tôi thậm chí không thể tạo di chuyển giản đồ sau khi di chuyển đó). Để khắc phục điều đó, chúng tôi có thể thay đổi ít lần sổ đăng ký ứng dụng mà bộ nối tiếp đang làm việc thành sổ đăng ký được cung cấp cho quá trình di chuyển trên tham số đầu tiên. Đăng ký đến đường dẫn được đặt tại django.core.serializers.python.apps.
GwynBleidD

3
Tại sao chúng ta lại làm việc này? Tại sao Django ngày càng trở nên khó vận hành và bảo trì? Tôi không muốn đi mặc dù điều này, tôi muốn có một giao diện dòng lệnh đơn giản giải quyết vấn đề này cho tôi, tức là nó đã từng xảy ra với đồ đạc. Django được cho là sẽ làm cho công việc này trở nên dễ dàng hơn chứ không phải khó hơn :(
CpILL 11/01

1
@GwynBleidD Đây là một điểm rất quan trọng mà bạn đang thực hiện và tôi nghĩ nó sẽ xuất hiện trong câu trả lời được chấp nhận này. Đó là nhận xét giống như nhận xét trong ví dụ mã di chuyển dữ liệu của tài liệu . Bạn có biết một cách khác để sử dụng trình tuần tự với biến được cung cấp app registrymà không thay đổi biến toàn cục (có thể gây ra sự cố trong tương lai giả định với việc di chuyển cơ sở dữ liệu song song).
Quảng cáo N

3
Câu trả lời này được ủng hộ cho kazoo cùng với sự chấp nhận chính là lý do tại sao tôi khuyên mọi người không nên sử dụng stackoverflow. Ngay cả bây giờ với các nhận xét và giai thoại, tôi vẫn có những người trong #django đề cập đến điều này.
shangxiao

50

Phiên bản ngắn

Bạn KHÔNG nên sử dụng loaddatalệnh quản lý trực tiếp trong quá trình di chuyển dữ liệu.

# Bad example for a data migration
from django.db import migrations
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # No, it's wrong. DON'T DO THIS!
    call_command('loaddata', 'your_data.json', app_label='yourapp')


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(load_fixture),
    ]

Phiên bản dài

loaddatasử dụng django.core.serializers.python.Deserializermô hình sử dụng các mô hình cập nhật nhất để giải mã dữ liệu lịch sử trong quá trình di chuyển. Đó là hành vi không đúng.

Ví dụ: giả sử rằng có một sự di chuyển dữ liệu sử dụng loaddatalệnh quản lý để tải dữ liệu từ một vật cố định và nó đã được áp dụng trên môi trường phát triển của bạn.

Sau đó, bạn quyết định thêm một trường bắt buộc mới vào mô hình tương ứng, vì vậy bạn thực hiện và thực hiện chuyển đổi mới đối với mô hình đã cập nhật của mình (và có thể cung cấp giá trị một lần cho trường mới khi ./manage.py makemigrationsbạn nhắc).

Bạn chạy lần di chuyển tiếp theo và tất cả đều ổn.

Cuối cùng, bạn đã phát triển xong ứng dụng Django của mình và bạn triển khai nó trên máy chủ sản xuất. Bây giờ đã đến lúc bạn chạy toàn bộ quá trình di chuyển từ đầu trên môi trường sản xuất.

Tuy nhiên, việc di chuyển dữ liệu không thành công . Đó là vì loaddatakhông thể lưu mô hình deserialized from , đại diện cho mã hiện tại, với dữ liệu trống cho trường bắt buộc mới mà bạn đã thêm. Vật cố định ban đầu thiếu dữ liệu cần thiết cho nó!

Nhưng ngay cả khi bạn cập nhật lịch thi đấu với dữ liệu cần thiết cho trường mới, việc di chuyển dữ liệu vẫn không thành công . Khi quá trình di chuyển dữ liệu đang chạy, quá trình di chuyển tiếp theo thêm cột tương ứng vào cơ sở dữ liệu vẫn chưa được áp dụng. Bạn không thể lưu dữ liệu vào một cột không tồn tại!

Kết luận: trong quá trình di chuyển dữ liệu,loaddatalệnh dẫn đến sự mâu thuẫn tiềm ẩn giữa mô hình và cơ sở dữ liệu. Bạn chắc chắn KHÔNG nênsử dụng nó trực tiếp trong quá trình di chuyển dữ liệu.

Giải pháp

loaddatalệnh dựa vào django.core.serializers.python._get_modelhàm để lấy mô hình tương ứng từ một vật cố định, sẽ trả về phiên bản cập nhật nhất của mô hình. Chúng ta cần phải vá nó để nó trở thành mô hình lịch sử.

(Đoạn mã sau hoạt động cho Django 1.8.x)

# Good example for a data migration
from django.db import migrations
from django.core.serializers import base, python
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # Save the old _get_model() function
    old_get_model = python._get_model

    # Define new _get_model() function here, which utilizes the apps argument to
    # get the historical version of a model. This piece of code is directly stolen
    # from django.core.serializers.python._get_model, unchanged. However, here it
    # has a different context, specifically, the apps variable.
    def _get_model(model_identifier):
        try:
            return apps.get_model(model_identifier)
        except (LookupError, TypeError):
            raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)

    # Replace the _get_model() function on the module, so loaddata can utilize it.
    python._get_model = _get_model

    try:
        # Call loaddata command
        call_command('loaddata', 'your_data.json', app_label='yourapp')
    finally:
        # Restore old _get_model() function
        python._get_model = old_get_model


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(load_fixture),
    ]

1
Rockallite, bạn tạo ra một điểm rất mạnh. Tuy nhiên, câu trả lời của bạn khiến tôi tự hỏi, liệu giải pháp 2.1 từ câu trả lời của @ n__o / @ mlissner dựa vào objects = serializers.deserialize('json', fixture, ignorenonexistent=True)vấn đề tương tự như loaddatathế nào? Hay ignorenonexistent=Truebao gồm tất cả các vấn đề có thể xảy ra?
Dário

7
Nếu bạn nhìn vào nguồn , bạn sẽ thấy rằng ignorenonexistent=Trueđối số có hai tác dụng: 1) nó bỏ qua các mô hình của một vật cố định không có trong các định nghĩa mô hình mới nhất, 2) nó bỏ qua các trường của mô hình một vật cố định không trong định nghĩa mô hình tương ứng mới nhất. Không ai trong số họ xử lý tình huống mới-bắt-buộc-trường-trong-mô-hình . Vì vậy, vâng, tôi nghĩ nó cũng gặp phải vấn đề tương tự như đơn giản loaddata.
Rockallite

Điều này hoạt động hiệu quả khi tôi phát hiện ra rằng json cũ của tôi có các mô hình tham chiếu các mô hình khác bằng cách sử dụng a natural_key(), mà phương pháp này dường như không hỗ trợ - tôi chỉ thay thế giá trị natural_key bằng id thực của mô hình được tham chiếu.
dsummersl 19/12/16

1
Có lẽ câu trả lời này là câu trả lời được chấp nhận sẽ hữu ích hơn, bởi vì khi chạy testcase, một cơ sở dữ liệu mới được tạo và tất cả các di chuyển được áp dụng từ đầu. Giải pháp này khắc phục các sự cố mà một dự án có tính năng đơn nhất sẽ gặp phải trong trường hợp không thay thế _get_model trong quá trình di chuyển dữ liệu. Tnx
Mohammad ali baghershemirani

Cảm ơn về bản cập nhật và giải thích, @Rockallite. Câu trả lời ban đầu của tôi được đăng vài tuần sau khi di chuyển được giới thiệu trong Django 1.7 và tài liệu về cách tiến hành không rõ ràng (và vẫn là lần trước tôi đã kiểm tra). Hy vọng rằng Django sẽ cập nhật cơ chế tải dữ liệu / di chuyển của họ để xem xét lịch sử mô hình một ngày nào đó.
n__o

6

Lấy cảm hứng từ một số nhận xét (cụ thể là n__o's) và thực tế là tôi có rất nhiều initial_data.*tệp nằm rải rác trên nhiều ứng dụng, tôi đã quyết định tạo một ứng dụng Django để tạo điều kiện thuận lợi cho việc tạo các di chuyển dữ liệu này.

Sử dụng django cư-vật cố bạn chỉ có thể chạy lệnh quản lý sau đây và nó sẽ tìm kiếm thông qua tất cả các bạn INSTALLED_APPScho initial_data.*file và biến chúng thành sự di cư của dữ liệu.

./manage.py create_initial_data_fixtures
Migrations for 'eggs':
  0002_auto_20150107_0817.py:
Migrations for 'sausage':
  Ignoring 'initial_data.yaml' - migration already exists.
Migrations for 'foo':
  Ignoring 'initial_data.yaml' - not migrated.

Xem django -igration-fixture để biết hướng dẫn cài đặt / sử dụng.


2

Để cung cấp cho cơ sở dữ liệu của bạn một số dữ liệu ban đầu, hãy viết một quá trình di chuyển dữ liệu. Trong quá trình di chuyển dữ liệu, hãy sử dụng chức năng RunPython để tải dữ liệu của bạn.

Không viết bất kỳ lệnh loaddata nào vì cách này không được dùng nữa.

Quá trình di chuyển dữ liệu của bạn sẽ chỉ được chạy một lần. Các cuộc di cư là một chuỗi di cư có thứ tự. Khi di chuyển 003_xxxx.py được chạy, di chuyển django ghi vào cơ sở dữ liệu rằng ứng dụng này được di chuyển cho đến khi ứng dụng này (003) và sẽ chỉ chạy các di chuyển sau.


Vì vậy, bạn khuyến khích tôi lặp lại các cuộc gọi đến myModel.create(...)(hoặc sử dụng một vòng lặp) trong chức năng RunPython?
Mickaël

khá nhiều, vâng. Cơ sở dữ liệu Transaactionnal sẽ xử lý nó một cách hoàn hảo :)
FlogFR

1

Rất tiếc, các giải pháp được trình bày ở trên không làm việc cho tôi. Tôi thấy rằng mỗi khi tôi thay đổi mẫu mã của mình, tôi phải cập nhật đồ đạc của mình. Lý tưởng nhất là thay vào đó, tôi sẽ viết các di chuyển dữ liệu để sửa đổi dữ liệu đã tạo và dữ liệu được tải cố định tương tự.

Để tạo điều kiện thuận lợi cho việc này, tôi đã viết một hàm nhanh sẽ tìm trong fixturesthư mục của ứng dụng hiện tại và tải một vật cố định. Đặt chức năng này vào một lần di chuyển tại điểm lịch sử mô hình khớp với các trường trong quá trình di chuyển.


Cảm ơn vì điều đó! Tôi đã viết một phiên bản hoạt động với Python 3 (và vượt qua Pylint nghiêm ngặt của chúng tôi). Bạn có thể sử dụng nó như một nhà máy với RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley,

1

Theo ý kiến ​​của tôi đồ đạc là một chút xấu. Nếu cơ sở dữ liệu của bạn thay đổi thường xuyên, việc cập nhật chúng sẽ sớm trở thành cơn ác mộng. Thực ra, đó không chỉ là ý kiến ​​của tôi, trong cuốn sách "Two Scoops of Django", nó được giải thích tốt hơn nhiều.

Thay vào đó, tôi sẽ viết một tệp Python để cung cấp thiết lập ban đầu. Nếu bạn cần thêm thứ gì đó, tôi sẽ đề nghị bạn xem Factory boy .

Nếu bạn cần di chuyển một số dữ liệu, bạn nên sử dụng di chuyển dữ liệu .

Ngoài ra còn có "Burn Your Fixtures, Use Model Factories" về cách sử dụng đồ đạc.


1
Tôi đồng ý trên quan điểm của bạn "khó có thể duy trì nếu thường xuyên thay đổi", nhưng ở đây các vật cố chỉ nhằm mục đích để cung cấp dữ liệu ban đầu (và tối thiểu) khi cài đặt dự án ...
Mickaël

1
Đây là lần tải dữ liệu một lần, nếu nó được thực hiện trong bối cảnh di chuyển sẽ có ý nghĩa. Vì nếu nó nằm trong một lần di chuyển, người ta không cần phải thực hiện các thay đổi đối với dữ liệu json. Bất kỳ thay đổi lược đồ nào yêu cầu thay đổi dữ liệu tiếp theo phải được xử lý thông qua một lần di chuyển khác (tại thời điểm đó, dữ liệu khác có thể trong cơ sở dữ liệu cũng sẽ cần được sửa đổi).
mtnpaul

0

Trên Django 2.1, tôi muốn tải một số mô hình (Ví dụ như tên quốc gia) với dữ liệu ban đầu.

Nhưng tôi muốn điều này xảy ra tự động ngay sau khi thực hiện quá trình di chuyển ban đầu.

Vì vậy, tôi nghĩ rằng sẽ thật tuyệt nếu có một sql/thư mục bên trong mỗi ứng dụng yêu cầu tải dữ liệu ban đầu.

Sau đó, trong sql/thư mục đó, tôi sẽ có .sqlcác tệp với các DML cần thiết để tải dữ liệu ban đầu vào các mô hình tương ứng, ví dụ:

INSERT INTO appName_modelName(fieldName)
VALUES
    ("country 1"),
    ("country 2"),
    ("country 3"),
    ("country 4");

Để mô tả rõ hơn, đây là cách một ứng dụng chứa một sql/thư mục sẽ trông như thế nào : nhập mô tả hình ảnh ở đây

Ngoài ra, tôi đã tìm thấy một số trường hợp mà tôi cần các sqltập lệnh được thực thi theo một thứ tự cụ thể. Vì vậy, tôi đã quyết định đặt tiền tố tên tệp bằng một số liên tiếp như trong hình trên.

Sau đó, tôi cần một cách để tải bất kỳ SQLscó sẵn nào bên trong bất kỳ thư mục ứng dụng nào một cách tự động bằng cách thực hiện python manage.py migrate.

Vì vậy, tôi đã tạo một ứng dụng khác có tên initial_data_migrationsvà sau đó tôi thêm ứng dụng này vào danh sách INSTALLED_APPStrong settings.pytệp. Sau đó, tôi tạo một migrationsthư mục bên trong và thêm một tệp có tên run_sql_scripts.py( Thực ra là một quá trình di chuyển tùy chỉnh ). Như được thấy trong hình ảnh dưới đây:

nhập mô tả hình ảnh ở đây

Tôi đã tạo run_sql_scripts.pyđể nó đảm nhận việc chạy tất cả các sqltập lệnh có sẵn trong mỗi ứng dụng. Cái này sau đó bị bắn khi ai đó chạy python manage.py migrate. Tùy chỉnh này migrationcũng thêm các ứng dụng có liên quan dưới dạng phụ thuộc, theo cách đó nó cố gắng chạy các sqlcâu lệnh chỉ sau khi các ứng dụng được yêu cầu đã thực hiện quá 0001_initial.pytrình di chuyển của chúng (Chúng tôi không muốn thử chạy một câu lệnh SQL với một bảng không tồn tại).

Đây là nguồn của script đó:

import os
import itertools

from django.db import migrations
from YourDjangoProjectName.settings import BASE_DIR, INSTALLED_APPS

SQL_FOLDER = "/sql/"

APP_SQL_FOLDERS = [
    (os.path.join(BASE_DIR, app + SQL_FOLDER), app) for app in INSTALLED_APPS
    if os.path.isdir(os.path.join(BASE_DIR, app + SQL_FOLDER))
]

SQL_FILES = [
    sorted([path + file for file in os.listdir(path) if file.lower().endswith('.sql')])
    for path, app in APP_SQL_FOLDERS
]


def load_file(path):
    with open(path, 'r') as f:
        return f.read()


class Migration(migrations.Migration):

    dependencies = [
        (app, '__first__') for path, app in APP_SQL_FOLDERS
    ]

    operations = [
        migrations.RunSQL(load_file(f)) for f in list(itertools.chain.from_iterable(SQL_FILES))
    ]

Tôi hy vọng ai đó thấy điều này hữu ích, nó hoạt động tốt cho tôi !. Nếu có thắc mắc gì xin cứ hỏi tôi.

LƯU Ý: Đây có thể không phải là giải pháp tốt nhất vì tôi mới bắt đầu với django, tuy nhiên tôi vẫn muốn chia sẻ "Cách thực hiện" này với tất cả các bạn vì tôi không tìm thấy nhiều thông tin khi tìm kiếm thông tin về điều này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.