単一のORMではない

単一のORMではない


みなさんこんにちは! Ostrovok.ruホテル予約サービスのパートナー開発部門を担当しています。 この記事では、1つのプロジェクトでDjango ORMをどのように使用したかについてお話したいと思います。


実際、私はだまされていました、名前は「 ない ORM single。「なぜ私がこれを書いたのか疑問に思っているなら、そして:



...猫へようこそ。


cdpv


2014年、B2B.Ostrovok.ruを開始しました。これは、観光市場の専門家(旅行代理店、オペレーター、企業顧客)向けのホテル、送迎、車、その他の旅行サービス向けのオンライン予約サービスです。


B2Bでは、 MetaOrder -meta order- MetaOrder基づいた抽象順序モデルを設計し、非常にうまく使用しています。


メタオーダーは、ホテル( Hotel )、追加サービス( Upsell )、または車( Car )に属する注文の種類に関係なく使用できる抽象的なエンティティです。 将来、他のタイプが表示される可能性があります。


これは常にそうではありませんでした。 B2Bサービスが開始されたとき、ホテルのみを予約でき、すべてのビジネスロジックはそれらに焦点を合わせていました。 たとえば、売上金額と予約払い戻し金額の為替レートを表示するために、多くのフィールドが作成されています。 時間が経つにつれて、メタオーダーが与えられると、このデータをどのように保存して再利用するのが最善かを実感しました。 しかし、コード全体を書き換えることはできず、この遺産の一部が新しいアーキテクチャに持ち込まれました。 実際、これにより、数種類の注文を使用する計算が困難になりました。 何をすべきか- 歴史的に ...


私の目標は、この例でDjango ORMの力を示すことです。


背景


費用を計画するために、B2Bのクライアントは、今/明日/後に支払う必要がある金額、注文に負債があるかどうか、そのサイズは何であるか、限度内でどれだけ多くを使うことができるかについての情報が本当に不足していました。 この情報をダッシュ​​ボードの形式で表示することにしました-わかりやすい図のようなシンプルなパネルです。


ダッシュ1
(すべての値はテストであり、特定のパートナーには適用されません)


一見、すべてが非常に簡単です-パートナーのすべての注文をフィルタリングし、要約して表示します。


ソリューションオプション


計算方法の簡単な説明。 当社は国際企業であり、さまざまな国のパートナーがさまざまな通貨で予約(購入および再販)を行っています。 さらに、選択した通貨(通常は現地通貨)で財務諸表を受け取る必要があります。 すべての通貨のレートに関するすべての可能なデータを保存するのは愚かで非実用的であるため、ルーブルなどの参照通貨を選択する必要があります。 したがって、すべての通貨のレートをルーブルにのみ保存できます。 したがって、パートナーが概要の受け取りを希望する場合、販売時に設定されたレートで金額を変換します。


「額に」


実際、これはModel.objects.all()あり、条件ループです:


Model.objects.all()条件付き
 def output(partner_id): today = dt.date.today() # query_get_one -    partner = query_get_one(Partner.objects.filter(id=partner_id)) #    -  query = MetaOrder.objects.filter(partner=partner) result = defaultdict(Decimal) for morder in query: #  ,     #     payment_pending = morder.get_payment_pending() payment_due = morder.get_payment_due() #        # (     ) payable = morder.get_payable_in_cur() #       if payment_pending > today: result['payment_pending'] += payable # ,     if payment_pending < today and payment_due > today: result['payment_due'] += payable return result 

このクエリは、数百の予約を潜在的に含むジェネレーターを返します。 これらの予約ごとにベースへのリクエストが行われるため、サイクルは非常に長い間続きます。


prefetch_relatedメソッドを追加することで、少し速度を上げることができます:


 # object -      GenericForeignKey. query = query.prefetch_related('object') 

その後、データベースへのクエリはわずかに少なくなります( GenericForeignKeyします)が、とにかく、ループの各反復でデータベースへのクエリが行われるため、それらの数で停止します。


outputメソッドはキャッシュできますが、キャッシュする必要がありますが、最初の呼び出しは1分間の順序を満たしますが、これはまったく受け入れられません。


このアプローチの結果は次のとおりです。


タイミング前


平均応答時間は4秒で、21秒に達するピークがあります。 かなり長い時間。


すべてのパートナーに対してダッシュボードを公開しなかったため、多くのリクエストはありませんでしたが、このアプローチが効果的でないことを理解するには十分です。


count_before
右下の数字は、クエリの数(最小、最大、平均、合計)です。


賢く


額のプロトタイプは、タスクの複雑さを理解するのには適していましたが、使用には最適ではありませんでした。 データベースに複数の複雑なクエリを作成する方が、多くの単純なクエリよりもはるかに高速でリソース集約的でないと判断しました。


リクエストプラン


クエリプランの広いストロークは、次のように記述できます。



初期条件


サイトにアクセスしたパートナーは、契約に関する情報のみを見ることができます。


 partner = query_get_one(Partner.objects.filter(id=partner_id)) 

新しいタイプの注文/予約を表示したくない場合は、サポートされているもののみをフィルタリングする必要があります。


 query = MetaOrder.objects.filter( partner=partner, content_type__in=[ Hotel.get_content_type(), Car.get_content_type(), Upsell.get_content_type(), ] ) 

注文ステータスは重要です( Qについての詳細):


 query = query.filter( Q(hotel__status__in=['completed', 'cancelled']) #     ,    # | Q(car__status__in=[...]) ) 

また、多くの場合、事前に準備されたリクエストを使用して、支払いできないすべての注文を除外します。 非常に多くのビジネスロジックがあり、この記事のフレームワークではあまり興味深いものではありませんが、本質的にこれらは単なる追加のフィルターです。 準備されたクエリを返すメソッドは次のようになります。


 query = MetaOrder.exclude_non_payable_metaorders(query) 

ご覧のとおり、これはQuerySetクラスメソッドです。


また、条件付き構築と計算結果の保存のためにいくつかの変数を準備します。


 import datetime as dt from typing.decimal import Decimal today = dt.date.today() result = defaultdict(Decimal) 

フィールド準備( annotate


注文のタイプに応じてフィールドを参照する必要があるため、 Coalesceを使用します。 したがって、1つのフィールドで任意の数の新しいタイプの注文を抽象化できます。


annotateブロックの最初の部分は次のとおりです。


最初の注釈
 #     , #      from app.helpers.numbers import ZERO, ONE query_annoted = query.annotate( _payment_pending=Coalesce( 'hotel__payment_pending', 'car__payment_pending', 'upsell__payment_pending', ), _payment_due=Coalesce( 'hotel__payment_due', 'car__payment_due', 'upsell__payment_due', ), _refund=Coalesce( 'hotel__refund', Value(ZERO) ), _refund_currency_rate=Coalesce( 'hotel__refund_currency_rate', Value(ONE) ), _sell=Coalesce( 'hotel__sell', Value(ZERO) ), _sell_currency_rate=Coalesce( 'hotel__sell_currency_rate', Value(ONE) ), ) 

ホテルの注文にはいくつかの特別なプロパティがあり、他のすべての場合(追加のサービスと車)では、これらのプロパティは重要ではないため、 Coalesceはここで強打します。 これが、金額のValue(ZERO) Value(ONE)と為替レートのValue(ONE)表示方法です。 ZEROおよびONEDecimal('0')およびDecimal(1) 、定数の形式のみです。 アマチュアのアプローチですが、私たちのプロジェクトでは慣習的です。


質問があるかもしれません。いくつかのフィールドをメタ順で1レベル上に上げてみませんか? たとえば、どこにでもあるpayment_pendingです。 実際、時間の経過とともに、そのようなフィールドをメタオーダーに転送しますが、コードはうまく機能するため、そのようなタスクは優先事項ではありません。


別の準備と計算


ここで、最後のannotateブロックで受け取った量を使用して計算する必要があります。 ここで、注文の種類に縛られる必要はもうありません(1つの例外を除く)。


2番目の注釈
 .annotate( #  _base     _sell_base=( F('_sell') * F('_sell_currency_rate') ), _refund_base=( F('_refund') * F('_refund_currency_rate') ), _payable_base=( F('_sell_base') - F('_refund_base') ), _reporting_currency_rate=Case( When( content_type=Hotel.get_content_type(), then=RawSQL( '(hotel.currency_data->>%s)::numeric', (partner.reporting_currency,), ), ), output_field=DecimalField(), default=Decimal('1'), ), ) 

このブロックの最も興味深い部分は、 _reporting_currency_rateフィールド、または販売時の参照通貨への為替レートです。 ホテル注文の参照通貨に対するすべての通貨の為替レートのデータは、currency_dataに保存されcurrency_data 。 これは単なるJSONです。 なぜこれを保持するのですか? これは歴史的にそうです。


そして、ここでは、なぜFを使用して契約通貨の価値を代用しないのでしょうか? つまり、これを行うことができればクールです:


 F(f'currency_data__{partner.reporting_currency}') 

ただし、 F f-strings Fではサポートされf-stringsいませんF Django ORMが既にネストされたjsonフィールドにアクセスする機能を持っているという事実は非常に喜ばしいことですF('currency_data__USD')


最後のannotateブロックは_payable_in_cur計算であり、すべての注文に対して合計されます。 この値は契約通貨である必要があります。


ダッシュ2


 .annotate( _payable_in_cur=( F('_payable_base') / F('_reporting_currency_rate') ) ) 

annotateメソッドの特徴は、リクエストに直接関係しないSELECT something AS something_elseコンストラクトを大量に生成することです。 これは、SQL query- query.__str__()アンロードすることで確認できます。


これはbase_query_annotated用にDjango ORMによって生成され SQLコードのbase_query_annotated 。 クエリをどこで最適化できるかを理解するには、かなり頻繁に読む必要があります。


最終計算


aggregate小さなラッパーがあるため、将来、パートナーが他のメトリックを必要とする場合、簡単に追加できます。


ダッシュ3


 def _get_data_from_query(query: QuerySet) -> Decimal: result = query.aggregate( _sum_payable=Sum(F('_payable_in_cur')), ) return result['_sum_payable'] or ZERO 

そしてもう1つ-これはビジネス条件による最後のフィルタリングです。たとえば、すぐに支払う必要があるすべての注文が必要です。


ダッシュ4


 before_payment_pending_query = _get_data_from_query( base_query_annotated.filter(_payment_pending__gt=today) ) 

デバッグと検証


作成されたクエリの正確性を検証する非常に便利な方法は、それをより読みやすいバージョンの計算と比較することです。


 for morder in query: payable = morder.get_payable_in_cur() payment_pending = morder.get_payment_pending() if payment_pending > today: result['payment_pending'] += payable 

「額」方式を認識していますか?


最終コード


その結果、次のようなものが得られました。


最終コード
 def _get_data_from_query(query: QuerySet) -> tuple: result = query.aggregate( _sum_payable=Sum(F('_payable_in_cur')), ) return result['_sum_payable'] or ZERO def output(partner_id: int): today = dt.date.today() partner = query_get_one(Partner.objects.filter(id=partner_id)) query = MetaOrder.objects.filter(partner=partner, content_type__in=[ Hotel.get_content_type(), Car.get_content_type(), Upsell.get_content_type(), ]) result = defaultdict(Decimal) query_annoted = query.annotate( _payment_pending=Coalesce( 'hotel__payment_pending', 'car__payment_pending', 'upsell__payment_pending', ), _payment_due=Coalesce( 'hotel__payment_due', 'car__payment_due', 'upsell__payment_due', ), _refund=Coalesce( 'hotel__refund', Value(ZERO) ), _refund_currency_rate=Coalesce( 'hotel__refund_currency_rate', Value(Decimal('1')) ), _sell=Coalesce( 'hotel__sell', Value(ZERO) ), _sell_currency_rate=Coalesce( 'hotel__sell_currency_rate', Value(Decimal('1')) ), ).annotate( # Calculated fields _sell_base=( F('_sell') * F('_sell_currency_rate') ), _refund_base=( F('_refund') * F('_refund_currency_rate') ), _payable_base=( F('_sell_base') - F('_refund_base') ), _reporting_currency_rate=Case( # Only hotels have currency_data, therefore we need a # check and default value When( content_type=Hotel.get_content_type(), then=RawSQL( '(hotel.currency_data->>%s)::numeric', (partner.reporting_currency,), ), ), output_field=DecimalField(), default=Decimal('1'), ), ) .annotate( _payable_in_cur=( F('_payable_base') / F('_reporting_currency_rate') ) ) before_payment_pending_query = _get_data_from_query( base_query_annotated.filter(_payment_pending__gt=today) ) after_payment_pending_before_payment_due_query = _get_data_from_query( base_query_annotated.filter( Q(_payment_pending__lte=today) & Q(_payment_due__gt=today) ) ) 

これが今の仕組みです:


タイミング後


count_after


結論


ロジックを書き直して最適化することで、アフィリエイトメトリックをかなり迅速に処理し、データベースへのクエリの数を大幅に削減することができました。 ソリューションは良好であることが判明し、このロジックをプロジェクトの他の部分で再利用します。 ORMは私たちのすべてです。


コメントを書いて、質問してください-私たちは答えようとします! よろしくお願いします!



Source: https://habr.com/ru/post/J447706/


All Articles