3

私は次の関数を書きました:

def auto_update_ratings(amounts, assessment_entries_qs, lowest_rating=-1):
    start = 0
    rating = lowest_rating
    ids = assessment_entries_qs.values_list('id', flat=True)

    for i in ids: # I have absolutely no idea why this seems to be required:
        pass      # without this loop, the last AssessmentEntries fail to update 
                  # in the following for loop.

    for amount in amounts:
        end_mark = start + amount
        entries = ids[start:end_mark]
        a = assessment_entries_qs.filter(id__in=entries).update(rating=rating)
        start = end_mark
        rating += 1

これは、想定されていることを実行します (つまり、 で指定されているようにassessment_entries_qs、各評価 ( から始まる) で関連する数のエントリを更新します)。簡単な例を次に示します。lowest_ratingamounts

>>> assessment_entries = AssessmentEntry.objects.all()
>>> print [ae.rating for ae in assessment_entries]
[None, None, None, None, None, None, None, None, None, None]
>>>
>>> auto_update_ratings((2,4,3,1), assessment_entries, 1)
>>> print [ae.rating for ae in assessment_entries]
[1, 1, 2, 2, 2, 2, 3, 3, 3, 4]

idsただし、 を反復する前に反復しない場合amounts、関数はクエリセットのサブセットのみを更新します。現在のテスト データ (クエリセットの約 250 AssessmentEntries) では、常に正確に 84AssessmentEntriesが更新されません。

興味深いことに、更新を行わないのは常に 2 番目の for ループの最後の反復 (ただし、その反復の残りのコードは適切に実行されます)と、前の反復の一部です。クエリセットは、この関数に渡される前に、ordered_by('?') であり、以前の「空の」for ループを追加するだけで意図した結果が得られるため、データの問題ではないようです)。

関連があることが判明した場合に備えて、さらにいくつかの詳細を示します。

  • AssessmentEntry.ratingが目安IntegerField(null=True,blank=True)です。
  • この関数は純粋にテスト目的で使用しているため、iPython からのみ実行しています。
  • テスト データベースは SQLite です。

質問:ids実際にはデータにまったく触れていないにもかかわらず、を反復処理する必要があるように見える理由と、そうしないと関数が (ある程度) 正しく実行されるのに、最後のいくつかの項目の更新に常に失敗する理由を説明してくださいどうやらまだそれらを繰り返しているにもかかわらず、クエリセット?

4

1 に答える 1

4

QuerySet と QuerySet スライスは遅延評価されます。ID を反復すると、クエリが実行され、 idsQuerySet ではなく静的リストのように動作します。したがって、 をループするidsと、entries後で が固定された値のセットになります。をループしない場合はids、実行するスライスを表す句が追加されentriesた単なるサブクエリになります。LIMIT

詳細は次のとおりです。

def auto_update_ratings(amounts, assessment_entries_qs, lowest_rating=-1):
    # assessment_entries_qs is an unevaluated QuerySet
    # from your calling code, it would probably generate a query like this:
    # SELECT * FROM assessments ORDER BY RANDOM()
    start = 0
    rating = lowest_rating
    ids = assessment_entries_qs.values_list('id', flat=True)
    # ids is a ValueQuerySet that adds "SELECT id"
    # to the query that assessment_entries_qs would generate.
    # So ids is now something like:
    # SELECT id FROM assessments ORDER BY RANDOM()

    # we omit the loop

    for amount in amounts:
        end_mark = start + amount
        entries = ids[start:end_mark]
        # entries is now another QuerySet with a LIMIT clause added:
        # SELECT id FROM assessments ORDER BY RANDOM() LIMIT start,(start+end_mark)
        # When filter() gets a QuerySet, it adds a subquery
        a = assessment_entries_qs.filter(id__in=entries).update(rating=rating)
        # FINALLY, we now actually EXECUTE a query which is something like this:
        # UPDATE assessments SET rating=? WHERE id IN 
        # (SELECT id FROM assessments ORDER BY RANDOM() LIMIT start,(start+end_mark))
        start = end_mark
        rating += 1

のサブクエリは、挿入するたびにentries実行され、ランダムな順序であるため、行うスライスは無意味です! この関数には、確定的な動作はありません。

ただし、id を反復すると、実際にクエリを実行するため、スライスは再び決定論的な動作を持ち、コードは期待どおりに動作します。

代わりにループを使用するとどうなるか見てみましょう。

ids = assessment_entries_qs.values_list('id', flat=True)

# Iterating ids causes the query to actually be executed
# This query was sent to the DB:
# SELECT id FROM assessments ORDER BY RANDOM()
for id in ids:
    pass

# ids has now been "realized" and contains the *results* of the query
# e.g., [5,1,2,3,4]
# Iterating again (or slicing) will now return values rather than modify the query

for amount in amounts:
    end_mark = start + amount
    entries = ids[start:end_mark]
    # because ids was executed, entries contains definite values
    # When filter() gets actual values, it adds a simple condition
    a = assessment_entries_qs.filter(id__in=entries).update(rating=rating)
    # The query executed is something like this:
    # UPDATE assessments SET rating=? WHERE id IN (5,1)
    # "(5,1)" will change on each iteration, but it will always be a set of
    # scalar values rather than a subquery.
    start = end_mark
    rating += 1

何もしない反復を実行するのではなく、その時点ですべての値を取得するために QuerySet を積極的に評価する必要がある場合は、リストに変換するだけです。

    ids = list(assessment_entries_qs.values_list('id', flat=True))

また、Django のドキュメントでは、正確に aQuerySetが評価されるタイミングについて詳しく説明しています。

于 2013-02-17T00:12:52.917 に答える