LEFT JOINを探しているDjango-orm

Django-ORMが一般的にスティックとして愚かであり、多少なりとも深刻なタスクを解決できないことはもはや秘密ではありません。特に、外部からの合理的なSQLクエリの形成に影響を与える必要がある場合は愚かです。 これらのケースの1つと、私がそれをどのように処理しようとしたかについて-カットの下で説明します。



すべては、TecDocデータベースを選択することで、データベースに翻訳ストレージシステムを実装するというアイデアに着想を得たという事実から始まりました。 ためらうことなく、私はそのようなモデルを翻訳の適用といじめのために投げました:



class Translations(models.Model): "  " text = models.TextField(null=True, blank=True) lng = models.SlugField(max_length=32, choices=settings.LANGUAGES, db_index=True) des = models.ForeignKey("Designations", db_index=True, related_name='translations') class Meta: verbose_name = _("translation") verbose_name_plural = _("translations") ordering = ['lng'] # db_table='mlang_translations' class Designations(models.Model): "  ()     id" class Meta: verbose_name = _("designation") verbose_name_plural = _("designations") # db_table='mlang_designations' class Page(MPTTModel): content = models.ForeignKey('mlang.Designations', null=True, blank=True, related_name="+") keywords = models.ForeignKey('mlang.Designations', null=True, blank=True, related_name="+") description = models.ForeignKey('mlang.Designations', null=True, blank=True, related_name="+") title = models.ForeignKey('mlang.Designations', null=True, blank=True, related_name="+") code = models.CharField(max_length=256, db_index=True) parent = TreeForeignKey('self', null=True, blank=True) # db_table='flatpages_page'
      
      







次のように機能します。



多くのモデルは翻訳ラベルを参照し、その後、いずれかの言語のフィールドの翻訳を取得できます。 最も単純な場合のクエリの数は、 1 + , *





モデルの説明からわかるように、翻訳済みフィールドと翻訳自体は同じラベルを参照しているため、翻訳を目的のフィールドに直接結合するため、翻訳を選択するときにラベル自体を簡単に強制できます。 そして、ここからタンバリンと踊り始めます。



「額」オプションから始めましょう。これは、他のオプションがない場合は使用しない方が良いです: QuerySet.rawそして、次のコードを取得します:



 Page.objects.raw(""" select fpage.id id, content_translated.text content_translated, title_translated.text title_translated, keywords_translated.text keywords_translated, description_translated.text description_translated from flatpages_page fpage left join mlang_translations content_translated on fpage.content_id=content_translated.des_id and content_translated.lng=%s left join mlang_translations description_translated on fpage.description_id=description_translated.des_id and description_translated.lng=%s left join mlang_translations keywords_translated on fpage.keywords_id=keywords_translated.des_id and keywords_translated.lng=%s left join mlang_translations title_translated on fpage.title_id=title_translated.des_id and title_translated.lng=%s """, params=["ru", "ru", "ru", "ru"])
      
      







このアプローチの長所と短所を描く必要はないと思います。

当然、多くのモデルがあり、複数のビューで翻訳を受信する必要がある場合、および/またはある時点でフィールドが変更される場合、これは現実には悪夢になります。



トピックdjango orm left join



グーグルを積極的に開始して、同等のSQLクエリを取得しますが、python / djangoの方法を使用します。



私が最初に目を引いたのは、Qオブジェクトを偽造してLEFT JOINに変えることでしたQLeftOuterJoin 、そしてもう少し長く慎重にグーグルで検索すると 、このソリューションはマンモスとして古く、2010年以降は機能していません。 起動の試みは失敗しました。



次に、Google検索でQuerySet.queryを介して特定の「 ハック 」が発生します。これにより、標準的な方法でカスタムINNER / LEFT JOINをQuerySetに埋め込むことができ、実験的な選択ではコードは次のようになります。



  qs = Page.objects.filter(id__isnull=False) # ,    . for field in Page._meta.local_fields: if field.rel is not None and field.rel.to is Designations: join = qs.query.join( (Page._meta.db_table, Translations._meta.db_table, ((field.name+'_id', 'des_id'),)), nullable=True, #  LEFT JOIN join_field=Translations._meta.get_field_by_name('des')[0] ) qs = qs.extra( select={ field.name+"_translated": join+'.text' }, where=[join+".lng=%s"], params=['ru'] )
      
      







ここで何が起こっているかを説明します。ページモデルのすべてのフィールドをソートし、ForeignKey(指定)ごとに一意のJOINを生成します。 docstring query.joinは次のように言います:

'join_cols'は、結合する列を含むタプルのタプルです((l_id1、r_id1)、(l_id2、r_id2))


つまり 最初の引数の3番目の要素では、条件に多くの接続フィールドを渡すことができますが、JOIN内でフィルタリングを行うことはできません。 その結果、qs.extra呼び出しでwhereとparamが出現し、すべてのLEFT JOINが次の形式の通常のINNER JOINに分割されました。



 SELECT ... FROM flatpages_pages, mlang_translations t1, mlang_translations_t2, ..... where t1.lng='ru' AND t2.lng='ru' AND ......
      
      







一方で、私たちは言うことができます-これは、1つのフィールドが翻訳されていない場合、機能であり、レコード全体を非表示にして404yuを与えます。 一方、これはデフォルトで必要な動作ではありません。



さて、ドキュメントに記載されている通常のdjangoの方法であるQuerySet.extraに進み、目的のモデルへの翻訳を自動的に生成するための補助関数を作成します。



 def translate(model, lng, exclude=None): if exclude is not None and not isinstance(exclude, (list, tuple, set, frozenset,)): raise TypeError('exclude must be iterable') fields = [] for field in model._meta.fields: if field.rel is not None and field.rel.to is Designations: if exclude is not None and field.name in exclude: continue fields.append( [field.name, map(lambda x: x[1], field.rel.get_joining_columns())[0]] ) if not fields: return {} return dict( tables=[ '"{trans._meta.db_table}" AS "trans_{pos}"'.format(trans=Translations, pos=pos) for pos, val in enumerate(fields) ], select={ column[0] + "_translated": "trans_{0}.text".format(pos) for pos, column in enumerate(fields) }, where=[ "{model._meta.db_table}.{column[1]}=trans_{pos}.des_id and trans_{pos}.lng=%s".format(pos=pos, column=column, model=model) for pos, column in enumerate(fields) ], params=[lng] * len(fields) )
      
      







それは非常に簡単に動作します:転送されたモデルからすべてのForeignKey(指定)を反復処理し、QuerySet.extraに転送するためにディクショナリに入力します。結果は次の呼び出しになります。



 Page.objects.extra(**translate(Page, lng))
      
      







見た目は美しいですが、これらは、段落2のプロファイルと同じ卵のみです。リクエストテキストには純血種の内部結合のみが含まれています...



upd:約束どおり、新しい最終的な検索結果で記事を補足します。



上記の方法はすべて、主にドキュメントといくつかの「ハッキング」に依存しており、一般的にどのように機能するかについて詳しくは説明していません。

そのため、特にdjango.db.models.sql.queryのQueryクラスのソースを見ると、辞書alias_mapおよびjoin_mapでの動作が弱いわけではないことがわかります。 join_mapは、タプルがキーであり、joinを呼び出すときに最初の引数として渡すキーであり、値はリクエスト内のジョインを正確に識別するためのエイリアスのタプルである通常の辞書にすぎません。 alias_mapはキーと同じエイリアスを使用し、JOIN自体のハンドルを値として使用します。これは後でSQLに変換されます。 記述子のタイプと形式は次の形式に縮小されます。

 JoinInfo = namedtuple('JoinInfo', 'table_name rhs_alias join_type lhs_alias ' 'join_cols nullable join_field')
      
      





SQLへの変換自体はハードワイヤードです。これにより、JOINをよりスマートに生成する機能を追加せずに、誰かがdjangoの腸にパッチを適用する可能性が完全に排除されます。

 LEFT OUTER JOIN table alias ON main_table.field=alias.field
      
      







しかし、重要なものが1つありますが、

エレガントなソリューションを追求する中で、アイテム2からの左結合が全体として完全に機能し、すべての要件を満たしているが、少し不満であるという事実を見落とすことができました。 すべてのSQL教科書に書かれているように、LEFT JOINは、欠落しているすべての正しいサンプルをNULLに置き換えます。したがって、n2が記事の冒頭で説明したものと同等の適切なJEFT JOINになるようにWHERE句を拡張できます。 n2のコードは次のようになります。

  qs = Page.objects.filter(id__isnull=False) #    for field in Page._meta.local_fields: if field.rel is not None and field.rel.to is Designations: alias = qs.query.join( (Page._meta.db_table, Translations._meta.db_table, ((field.name+"_id", 'des_id',),)), nullable=True, join_field=Translations._meta.get_field_by_name("des")[0] ) qs = qs.extra( select={field.name+"_translated": alias+'.text'}, where=["{0}.lng='{1}' or {0}.lng is null".format(alias, 'ru')], #  or lng is null )
      
      





出力では、Query.rawからSQLとまったく同じデータを取得します。ここで言語フィルタリングを移動することでWHEREをより複雑にしましたが、最終的なSQLの可読性が低下し、これが元のデータベースレベルでのSQL実行速度にどのように影響するかはわかりません 一般に、問題は解決されたと言えます。純粋なSQLを記述することなくデータベースへのよりインテリジェントなクエリを作成することで、データベースへの膨大な数の不要なクエリを取り除くことができました。これにより、異なるDBMS間のコードの移植性が保証されます。



PS:この小さな研究が誰かがよりよく眠れるようになることを願っています。

ソースはgithub.comにあります。



All Articles