ジャンゴORM。 砂糖を加える





Djangoフレームワークは、おそらくPython言語で最も人気があります。 ただし、その人気のために、そのORMはしばしば批判されます-つまり、アンダースコアを介した検索構文です。 実際、このような構文の選択は非常に正当化されています-理解しやすく、拡張可能で、最も重要なのは-モップのように単純です。 それにもかかわらず、人は美しさ、または簡単な恵みさえ望んでいます。 しかし、美しさは相対的な概念であるため、特定のタスクから反発されます。 あなたが興味を持っている場合-猫へようこそ。



実際、アンダースコアを使用した検索には、主に2つの欠点があります。

1.十分に長い場合、文字列の可読性が低い。

例:



>>> query = SomeModel.objects.filter(user__savepoint__created_datetime__gte=last_week)
      
      





一見、created_datetimeとcreated__datetimeを混同したり、その逆を行ったりするため、ユーザーとセーブポイントの間に下線を引くことはできません。 さらに、ルックアップパラメーターはモデルフィールドと混同される可能性があります。 もちろん、綿密な検査で「以上」の意味が明らかになりますが、コードを読むと、貴重な時間が失われます。



2.検索バーを再利用することは困難です。 上記のクエリを例にとり、created_datetimeフィールドで結果を並べ替えてみてください。



 >>> query.order_by('user__savepoint__created_datetime')
      
      





ご覧のとおり、長い文字列を再入力する必要があり、変数として保存することはできません。1つのケースでは、文字列と上記のキーワード引数を使用するためです。



気難しい読者は、文字列の主要部分を変数query_param = 'user__savepoint_created_datetime'に保存し、そのようなハックを作成できることに気付くでしょう。



 >>> SomeModel.objects.filter(**{'{}_gte'.format(query_param): last_week}).order_by(query_param)
      
      





しかし、そのようなコードはさらに混乱を招きます。つまり、リファクタリングの主なタスクは、コードを単純化することであり、完成していません。

最初の段落から、アンダースコアを何らかの方法でドットに置き換える必要があります。 また、比較演算(__eq __、__ gt __、__ lt__など)の動作をオーバーライドできることもわかっています。

同様に使用するには:



 >>> SomeModel.user.savepoint.created_datetime >= last_week
      
      





ただし、モデルとフィールドのチャンクラスにパッチを適用する必要があるため、最初のソリューションはそれほど素晴らしいものではありません。これは、現実世界でのソリューションの使用に既に大きな制限を課しています。 さらに、モデル名は非常に長くなる可能性があるため、複数のフィルターを組み合わせる必要がある場合、ソリューションは冗長になりすぎます。 Qクラスは私たちの助けになります-非常に短く、余分なものは何も含まれていません。 そのようなことをしましょう-それをS(Sugarから)と呼びます。 これを使用して文字列を生成します。



 >>> S.user.savepoint.created_datetime >= last_week {'user__savepoint__created_datetime__gte': last_week}
      
      





ただし、それを使用することはまだそれほど便利ではありません。



 >>> SomeModel.objects.filter(**(S.user.savepoint.created_datetime >= last_week))
      
      





Qクラスが再び役立ちます-フィルタに直接渡すことができるため、完成したインスタンスを返します!



 >>> S.user.savepoint.created_datetime >= last_week Q(user__savepoint__created_datetime__gte=last_week) >>> SomeModel.objects.filter(S.user.savepoint.created_datetime >= last_week)
      
      





したがって、使用するAPIがあります。それは実装次第です。 せっかちな人は、すぐにgithub.com/Nepherhotep/django-orm-sugarリポジトリを開くことができます。



タスク番号1。 比較操作のオーバーライド



ここでドキュメントをdocs.python.org/2/reference/datamodel.html#object.__lt__で開き、使用可能な機能を確認します。 これらは__lt __、__ le __、__ gt __、__ ge __、__ eq __、__ ne__です。 対応するQオブジェクトを返すように、それらを再定義します。



 def __ge__(self, value): return Q(**{'{}__gte'.format(self.get_path()): value})  . .
      
      





ただし、isオペレーションは再定義できません; Pythonスタイルの包含をチェックするのも難しいでしょう:



 'substr' in S.user.username
      
      





したがって、このような操作では、同じ名前の関数を作成します。



 def contains(self, value): return Q(**{'{}__contains'.format(self.get_path()): value}) def in_list(self, value): return Q(**{'{}__value'.format(self.get_path()): value})
      
      





利便性のデモンストレーションとして、便利なin_rangeメソッドを追加します。



 def in_range(self, min_value, max_value): return (self <= min_value) & (self >= max_value)
      
      





その便利さは、2つのパラメーターを一度に渡すことができることです。これは、キーワード引数を使用するときには不可能でした。



 >>> SomeModel.objects.filter(S.user.savepoint.created_datetime.in_range(month_ago, week_ago))
      
      







タスク番号2。 ポイントアクセスでの子インスタンスの作成



 >>> S.user.savepoint.create_datetime
      
      





最初に、クラスではなくオブジェクトの属性を引き続き使用します。 ただし、上記ではコンストラクターを呼び出さずにクラスを使用したため、単純にモジュールレベルでオブジェクトを作成します。 第二に、元のクラス自体はより正気である-SugarQueryHelperと呼ばれます。



 class SugarQueryHelper(object): pass S = SugarQueryHelper()
      
      





その場で属性を生成するには、__ getattr__メソッドをオーバーライドする必要があります。他の方法で属性が見つからない場合、最後に呼び出されます。



  def __getattr__(self, item): return SugarQueryHelper()
      
      





ただし、渡されたパラメーターの名前も覚えておく必要があります。そのため、それに基づいてQオブジェクトと親クラスへのリンクを生成します。



 class SugarQueryHelper(object): def __init__(self, parent=None, name=''): self.__parent = parent self.__name = name def __getattr__(self, item): return SugarQueryHelper(self, item)
      
      





これで、パス生成の追加が完了し、モジュールの準備が整いました!



  def get_path(self): if self.__parent: parent_param = self.__parent.get_path() if parent_param: #  ,       return '__'.join([parent_param, self.__name]) #         return self.__name
      
      





現在、このメソッドはSugarQueryHelper内だけでなく、クエリ文字列をorder_byまたはselect_relatedに渡す必要がある場合にも使用できます。

これにより、上記の問題-クエリ文字列の再利用-が解決されることがわかります。



 >>> sdate = S.user.savepoint.created_datetime >>> SomeModel.filter(sdate >= last_week).order_by(sdate.get_path())
      
      







さらなる開発



実行は些細なものでしたが、モジュールは非常に優れていました。 しかし、改善できるものがあります。



よく見ると、Sオブジェクトは、補助関数と同じ方法で名前が付けられたフィールド(包含、アイコン、正確など)にアクセスすることを許可しません。もちろん、そのようにフィールドを呼び出すことを考える人はほとんどいませんが、マーフィーのようなケースはこれまでに起こりません。



ここで何ができますか? 作業スキームを少し変更します。__call__メソッドをオーバーライドします。呼び出された場合、パスの最後のオブジェクトの名前をスキップできます。 この場合、補助関数自体は下線で始まり、登録(下線なし)はデコレーターを介して行われます。



 @register_hook('icontains') def _icontains(self, value): return Q(...)
      
      





しかし、そのような決定は、状況によってはあまり明白ではないように思えました。 最後に、 ライブラリはQオブジェクトを生成するだけで、キーワードを使用する通常の方法はまだ利用できるため、この実装を変更しないことにしました(バージョンはspecial_namesブランチで利用可能です)。



次にできることは、Q互換性を実装することでした。 つまり SとQをインポートする代わりに、両方のケースでQのみを使用できます。 ただし、Qオブジェクトにはかなり複雑な実装があり、さらにライブラリユーザーには理解できないパブリックメソッドがあります。 問題を突然解決することは不可能だったので、私はそれをそのままにしておきました。



カスタマイズされたクエリマネージャーdocs.djangoproject.com/en/1.8/topics/db/managers/#custom-managersから同じフィルタリングを直接実行することもできます。 次に、それを再定義して、次の形式のリクエストを作成できます。



 >>> SomeModel.s_objects.user.age >= 16 <QuerySet>
      
      





ただし、これはジャンガアプローチのフレームワーク内で何が起こっているかを理解していない。 さらに、2つ以上のフィルターを組み合わせる必要がある場合、このアプローチの利点は完全に失われます。



 >>> (SomeModel.s_objects.user.age >= 16) & (SomeModel.s_objects.user.is_active == True) vs >>> SomeModel.objects.filter((S.user.age >= 16) & (S.user.is_active == True))
      
      





それほど簡単ではありませんか? 異なるモデルからのクエリを結合しようとすると、起こりうる問題は言うまでもありません-構文はそうします!



あとがき



ご覧のとおり、有用なものを書くことはそれほど難しくありません。 この場合、重い操作はすべてdzhangaの標準機能の肩にかかっています-結局、クエリマネージャー自身が、番号、日付、またはFオブジェクトが与えられたかどうか、フィールド名が正しいかどうかなどをチェックします。 また、モジュールはpythonの2番目と3番目のバージョンの両方で動作します。

誰かがアイデア、コメント、または提案を持っている場合-コメントを書くか、リクエストのプールを送信します。



ご清聴ありがとうございました!



All Articles