画面「多対一(参照)/一対多/多対多」テーブルの列 → SQL列 relation table

1) 種別

  • 画面: 種別(M2O / O2M / M2M バッジ)
  • SQL: rel_kind_raw
    • 値: 'many2one' | 'one2many' | 'many2many'
  • 画面の「日本語分類」見出し(多対一…など)
  • SQL: classification

2) こちらのフィールド

  • 画面: こちらのフィールド(例: partner_id)
  • SQL: local_field

3) 相手モデル

  • 画面: 相手モデル(例: res.partner)
  • SQL: remote_model
    • 元: ir_field_src.relation

4) 相手側キー

  • 画面: 相手側キー(スクショでは id が多い)
  • SQL: remote_key
    • many2one: 常に 'id'
    • one2many: inverse_field があればそれ(= 子テーブル側の外部キー)
    • many2many: 'id'(本質は junction 側で結ぶので、表示はid固定でOK)

画面の「相手側キー」は “JOINで使う相手側の結合キー” という意味で表示しているはずなので、O2Mだけは inverse_field を見せるのが自然です。


5) 用途/要約

  • 画面: 用途/要約(スクショでは日本語テキスト)
  • SQL: usage_summary_ja
    • 元: ir_field_src.notes(JSON/テキストどちらでもそのまま)
    • notes が i18n JSON の場合は UI 側で ja_JP を優先表示にするのが良いです

6) SQL結合ヒント

  • 画面: SQL結合ヒント(例: JOIN res_partner p ON so.partner_id = p.id
  • SQL: sql_join_hint
    • many2one: local_alias.local_field = remote_alias.id
    • one2many: local_alias.id = remote_alias.inverse_field(inverseが取れた時だけ)
    • many2many: local_alias.id = j.column1remote.id = j.column2(junctionが取れた時だけ)

ここは「ある時だけ出る」でOK(無いものは画面で埋める方針)。


画面には出てないが、UI/次工程で使える列(推奨)

A) モデル/テーブル情報(ページ文脈)

  • local_model(例: sale.order)
  • local_table(例: sale_order)
  • remote_table(例: res_partner など)
    • 元: ir_model_src.model_table(remote_model を引いて解決)

→ 画面で SQL結合ヒント を生成し直したい時や、JOINプランに流す時に必須。


B) 相手の表示フィールド(UIでリンク/サジェストに使える)

  • remote_display_field(name / display_name / id)
  • remote_display_label_ja(そのラベル)

→ スクショには無いですが、「相手側の代表表示」用途で便利。


C) O2M / M2M の追加情報(詳細に出すなら)

  • inverse_field, inverse_field_label_ja(O2M)
  • junction_table, junction_col_local, junction_col_remote(M2M)

D) 一意キー

  • natural_key(例: relation::sale.order::partner_id
    → フロント側で行IDに使える、差分更新にも強い。

まとめ:画面列を表示する最小セット

画面の表を成立させる最小はこれです:

  • rel_kind_raw(種別バッジ)
  • local_field(こちらのフィールド)
  • remote_model(相手モデル)
  • remote_key(相手側キー)
  • usage_summary_ja(用途/要約)
  • sql_join_hint(SQL結合ヒント)
CREATE OR REPLACE VIEW public.v_ir_model_relations AS
WITH local_fields AS (
  SELECT
    f.id,
    f.model       AS local_model,
    f.model_table AS local_table,
    f.field_name  AS local_field,
    lower(f.ttype) AS rel_kind_raw,

    f.label_i18n,
    f.notes,

    -- notes の中のカスタム(UI上書き)を参照
    CASE
      WHEN jsonb_typeof(f.notes) = 'object' THEN (f.notes->'__relation_custom')
      ELSE NULL
    END AS rel_custom,

    NULLIF(f.relation,'')        AS base_remote_model,
    NULLIF(f.relation_field,'')  AS base_inverse_field,
    NULLIF(f.relation_table,'')  AS base_junction_table,
    NULLIF(f.column1,'')         AS base_junction_col_local,
    NULLIF(f.column2,'')         AS base_junction_col_remote
  FROM public.ir_field_src f
  WHERE lower(f.ttype) IN ('many2one','one2many','many2many')
),

local_fields_resolved AS (
  SELECT
    lf.*,

    -- ★カスタムがあれば上書き、なければ src を採用(空文字は無視)
    COALESCE(NULLIF(lf.rel_custom->>'remote_model',''), lf.base_remote_model) AS remote_model,
    COALESCE(NULLIF(lf.rel_custom->>'inverse_field',''), lf.base_inverse_field) AS inverse_field,

    COALESCE(NULLIF(lf.rel_custom->>'junction_table',''), lf.base_junction_table) AS junction_table,
    COALESCE(NULLIF(lf.rel_custom->>'junction_col_local',''), lf.base_junction_col_local) AS junction_col_local,
    COALESCE(NULLIF(lf.rel_custom->>'junction_col_remote',''), lf.base_junction_col_remote) AS junction_col_remote
  FROM local_fields lf
),

local_field_label AS (
  SELECT
    lfr.id,
    COALESCE(
      NULLIF(lfr.label_i18n->>'ja_JP',''),
      NULLIF(lfr.label_i18n->>'en_US',''),
      lfr.local_field
    ) AS local_field_label_ja
  FROM local_fields_resolved lfr
),

remote_model_table AS (
  SELECT
    lfr.id,
    lfr.remote_model,
    COALESCE(NULLIF(rm.model_table,''), replace(lfr.remote_model,'.','_')) AS remote_table
  FROM local_fields_resolved lfr
  LEFT JOIN public.ir_model_src rm
    ON rm.model = lfr.remote_model
),

remote_display_pick AS (
  SELECT
    lfr.id,
    COALESCE(
      NULLIF(lfr.rel_custom->>'remote_display_field',''),
      CASE
        WHEN lfr.remote_model IS NULL THEN NULL
        WHEN EXISTS (
          SELECT 1 FROM public.ir_field_src rf
          WHERE rf.model = lfr.remote_model AND rf.field_name = 'name'
        ) THEN 'name'
        WHEN EXISTS (
          SELECT 1 FROM public.ir_field_src rf
          WHERE rf.model = lfr.remote_model AND rf.field_name = 'display_name'
        ) THEN 'display_name'
        ELSE 'id'
      END
    ) AS remote_display_field
  FROM local_fields_resolved lfr
),

remote_display_label AS (
  SELECT
    rdp.id,
    rdp.remote_display_field,
    COALESCE(
      NULLIF(rf.label_i18n->>'ja_JP',''),
      NULLIF(rf.label_i18n->>'en_US',''),
      rdp.remote_display_field
    ) AS remote_display_label_ja
  FROM remote_display_pick rdp
  LEFT JOIN local_fields_resolved lfr ON lfr.id = rdp.id
  LEFT JOIN public.ir_field_src rf
    ON rf.model = lfr.remote_model
   AND rf.field_name = rdp.remote_display_field
),

inverse_field_label AS (
  SELECT
    lfr.id,
    COALESCE(
      NULLIF(rf.label_i18n->>'ja_JP',''),
      NULLIF(rf.label_i18n->>'en_US',''),
      lfr.inverse_field
    ) AS inverse_field_label_ja
  FROM local_fields_resolved lfr
  LEFT JOIN public.ir_field_src rf
    ON rf.model = lfr.remote_model
   AND rf.field_name = lfr.inverse_field
)

SELECT
  CASE rel_kind_raw
    WHEN 'many2one'  THEN '多対一(参照)'
    WHEN 'one2many'  THEN '一対多(子テーブル)'
    WHEN 'many2many' THEN '多対多(中間テーブル)'
  END AS classification,

  rel_kind_raw,
  local_model,
  local_table,
  local_field,
  lfl.local_field_label_ja,

  lfr.remote_model,
  rmt.remote_table,

  CASE
    WHEN rel_kind_raw = 'many2one' THEN 'id'
    WHEN rel_kind_raw = 'one2many' THEN COALESCE(lfr.inverse_field, 'id')
    WHEN rel_kind_raw = 'many2many' THEN 'id'
  END AS remote_key,

  rdl.remote_display_field,
  rdl.remote_display_label_ja,

  -- ★usage_summary_ja は jsonb のまま(互換維持)
  -- 優先: custom.usage_summary_ja(文字列)→ notes.ja_JP → notes.en_US → local_field
  COALESCE(
    to_jsonb(NULLIF(lfr.rel_custom->>'usage_summary_ja','')),
    CASE WHEN jsonb_typeof(lfr.notes) = 'object' THEN to_jsonb(NULLIF(lfr.notes->>'ja_JP','')) END,
    CASE WHEN jsonb_typeof(lfr.notes) = 'object' THEN to_jsonb(NULLIF(lfr.notes->>'en_US','')) END,
    to_jsonb(lfr.local_field)
  ) AS usage_summary_ja,

  -- joinヒント:custom があればそれ、なければ計算値(text)
  COALESCE(
    NULLIF(lfr.rel_custom->>'sql_join_hint',''),
    CASE
      WHEN rel_kind_raw = 'many2one' AND rmt.remote_table IS NOT NULL THEN
        'JOIN ' || rmt.remote_table || ' r ON l.' || lfr.local_field || ' = r.id'
      WHEN rel_kind_raw = 'one2many' AND rmt.remote_table IS NOT NULL AND lfr.inverse_field IS NOT NULL THEN
        'JOIN ' || rmt.remote_table || ' r ON l.id = r.' || lfr.inverse_field
      WHEN rel_kind_raw = 'many2many'
        AND lfr.junction_table IS NOT NULL
        AND lfr.junction_col_local IS NOT NULL
        AND lfr.junction_col_remote IS NOT NULL
        AND rmt.remote_table IS NOT NULL THEN
        'JOIN ' || lfr.junction_table || ' j ON l.id = j.' || lfr.junction_col_local
        || ' JOIN ' || rmt.remote_table || ' r ON r.id = j.' || lfr.junction_col_remote
      ELSE NULL
    END
  ) AS sql_join_hint,

  lfr.inverse_field,
  ifl.inverse_field_label_ja,

  lfr.junction_table,
  lfr.junction_col_local,
  lfr.junction_col_remote,

  ('relation::' || lfr.local_model || '::' || lfr.local_field) AS natural_key
FROM local_fields_resolved lfr
LEFT JOIN local_field_label     lfl ON lfl.id = lfr.id
LEFT JOIN remote_model_table    rmt ON rmt.id = lfr.id
LEFT JOIN remote_display_label  rdl ON rdl.id = lfr.id
LEFT JOIN inverse_field_label   ifl ON ifl.id = lfr.id
;

Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です