。**「Odoo本番DBの ir_ui_view を devportal 側へミラー → portal_smart_button.view_id をミラー表にFK」**に寄せると、将来の整合性・保守性が一気に楽になります。
ここではコードは出さずに、やり方・判断ポイント・切替手順を“作業手順書”としてまとめます。
(前段の 422 や OpenAPI/ルータの混線は最後にチェックリスト化)
ミラーリングの選択肢(結論→おすすめ順)
- PostgreSQL FDW(postgres_fdw)+ローカル鏡テーブル(推奨)
- devportal DB から odoo DB を外部サーバとして参照し、
ir_ui_view/ir_model_dataを読み取り。 - それを devportal のローカル鏡テーブル(例:
public.ir_ui_view_mirror)に取り込み(初回フル/以後は差分Upsert)。 - FK はローカル鏡テーブルに張る(外部表にはFK張れないため)。
- 更新は定期ジョブ(K8s CronJob / pg_cron / 手動)で反映。運用・責務が明快。
- 論理レプリケーション(Publication/Subscription)
- odoo 側で
ir_ui_view/ir_model_dataを公開、devportal 側で購読しローカル表へ自動反映。 - ほぼリアルタイムだが、**権限(superuser 相当)**や接続要件がFDWより重い。運用の難易度◎→上級向け。
- ETLバッチ(Python or psql)
- アプリやCronJobで直クエリ→Upsert。自由度高いがメンテ工数が乗る。
- まずは FDW で安定運用→必要なら ETL に移行、が無難。
あなたの現在構成(同一Postgresインスタンス内に
odooとdevportalが同居)だと、FDWが一番シンプルで堅いです。
FDW 方式:手順(ノーコード版の運用手順書)
A. 事前設計
- 接続元/先
- 接続元:
devportalDB(ミラーを置く場所) - 接続先:
odooDB(ソース:ir_ui_view/ir_model_data)
- 接続元:
- 権限
- odoo側に読み取り専用ユーザ(
SELECTonir_ui_view,ir_model_data)。 - devportal側に拡張作成権限(
CREATE EXTENSION postgres_fdw)。
- odoo側に読み取り専用ユーザ(
- ミラー表の項目(最低限)
id(PK / Odooの view.id をそのまま)xmlid(一意 /ir_model_data:module.name連結。無い時はir_ui_view.keyを代替)name,model,type,inherit_id,active,priority,write_date- 変更検出用に
arch_hash(arch_dbのハッシュ)を持たせると差分検出が高速化 - 監査:
created_at,updated_at(自動更新トリガ)
xmlid の作り方:
ir_model_dataでmodel='ir.ui.view' AND res_id=view.idを結合し、module || '.' || nameを採用。無い場合はir_ui_view.keyをフォールバック。
B. 一度きりの初期設定
- FDW有効化(devportal 側で
postgres_fdwを有効に) - 外部サーバ定義(devportal→odoo への接続・DB名・ホスト・ポート)
- ユーザマッピング(読み取りユーザ+パスの関連付け)
- 外部テーブル定義 or スキーマインポート
- 最低限
ir_ui_viewとir_model_dataを外部化
- 最低限
- ローカル鏡テーブル作成(
public.ir_ui_view_mirror)- PK=
id、xmlid UNIQUE、必要な補助Index(write_dateなど)
- PK=
C. 初回フルロード
- 外部表の
ir_ui_viewとir_model_dataをJOINし、鏡テーブルへ一括投入。 xmlid重複は基本ないが、ユニーク制約違反はログし、人手で解決(最初に品質を揃えるのが肝)。
D. 差分反映(選択肢)
- 方法D-1:
write_date増分- devportal 側に最後の同期時刻を保持し、
write_date > last_syncの行のみUpsert。 - **削除(odoo 側で view 削除)**も扱うなら、
- a) フル比較(遅いが確実)、または
- b) odoo 側の削除履歴(無ければソフトデリート扱いでよしとする方針定義)。
- devportal 側に最後の同期時刻を保持し、
- 方法D-2:
arch_hash比較- 内容差分のみ再取り込み。
write_dateが緩い運用でも正確。
- 内容差分のみ再取り込み。
- 方法D-3:夜間フルREFRESH
- 件数が大きくなければ、夜間に全件入れ替え。簡単・堅牢。
- その場合は二相テーブル(
_staging→SWAP)で無停止切り替え。
スタート時は D-1(write_date増分)+ 週1回フル比較 が現実的です。
E. スケジューリング
- Kubernetes CronJob で devportal API 側に「同期実行」エンドポイントを用意して叩く、または
- pg_cron 拡張で devportal DB 内にスケジュール(DB内で完結)。
- 失敗時は リトライ+サーキットブレーカ(一定回数失敗で停止・通知)。
F. 監視・運用
- 件数照合(view 総数 / xmlid 欠損件数 / 重複件数)をメトリクス化。
- 代表サンプルの ランダム・スポットチェックを毎回 n 件。
- ログは 「追加 / 更新 / 削除 / スキップ(理由)」 の4分類で集計。
portal_smart_button のFK切り替え計画(ノーコード手順)
- 鏡テーブルが安定(初回フル+差分1回以上)してから進める。
- 既存の
portal_smart_button.view_idが portal_view(id) を指しているなら、移行マッピングが必要:portal_viewにもxmlidが持てるなら、portal_view.xmlid→ 鏡のir_ui_view_mirror.xmlidで突き合わせportal_smart_button.view_idを mirror の id に更新(移行スクリプト)
xmlidを持っていないなら、model + type + name等から推定し、最終的に人手確認。
- すべての
portal_smart_button.view_idが mirror 側の id になったら、- 旧FK(
portal_view向き)を削除 - 新FK(
ir_ui_view_mirror(id)向き)を追加(ON DELETE CASCADE)。
- 旧FK(
- 一斉テスト
POST /smart_buttons(view_xmlidのみ)で作成 → サーバ側でmirror解決→201GET /smart_buttonsで返るview_idがmirrorのidであることを確認- 代表的な view で存在しないxmlidや重複xmlid時のエラーメッセージを確認(422系の語彙統一)。
- OpenAPI 説明の明確化
view_idは Odooの view.id(=mirror.id) であることを記載view_xmlidはmodule.name形式、どちらか必須(one-of)- 両方渡されたら整合チェック(不一致は 409/422)。
OpenAPI/ルータまわりの“今の詰まり”チェックリスト
- SmartButtonCreate
view_id/view_xmlidは両方 Optional- **「どちらか必須」**は Pydantic の root-level バリデーションで実装(片方無ければエラー)。
POST時、view_xmlidが入れば mirror解決→view_idに変換(見つからない時は 422)。
- importの単一路線化
app.include_router(smart_buttons.router)に一本化。歴史的なrouter_altや古いパスを外す。
- /smart_buttons/_debug_schema
- 簡易GETで、現在の必須/型/one-of規約を JSON 返却(運用時の確認口)。
- /openapi.json の更新確認
- 新タグでビルド →
minikube image load --overwrite→kubectl set image→rollout status。 - ブラウザはキャッシュ無効化+ハードリロードで確認。
- 新タグでビルド →
- 翻訳の対象拡張
label_i18nに加えてnotes_i18nもonly_missing/dry_runのロジックに追加。
- 過去の 500 残滓
- 稼働中イメージのタグとコミットを確認し、修正がデプロイ済みかを棚卸し。
- ログに
insert_data 未定義・IndentationErrorがないことを直近で確認。
上記のAからCを今回は行う。
スマートボタンのみOdooの標準テーブルとの連携を確認する方法をとる。ほかはやっていない。スタンドアローンで連携していない状態。
-- ================================================
-- devportal DBで実行:Odoo ir_ui_view をミラー(FDW)
-- 資格情報は odoo-configmap の db_user / db_password を使用
-- host = postgres.infra.svc.cluster.local
-- dbname = odoo
-- port = 5432
-- user = odoo
-- pass = odoo
-- ================================================
-- A. 前提
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE OR REPLACE FUNCTION public.set_timestamp_updated_at()
RETURNS trigger AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- B-1. FDWサーバ定義(再作成に強い)
DROP SERVER IF EXISTS odoo_svr CASCADE;
CREATE SERVER odoo_svr
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (
host 'postgres.infra.svc.cluster.local',
dbname 'odoo',
port '5432'
);
-- B-1'. ユーザマッピング(odoo/odoo)
DROP USER MAPPING IF EXISTS FOR CURRENT_USER SERVER odoo_svr;
CREATE USER MAPPING FOR CURRENT_USER
SERVER odoo_svr
OPTIONS (user 'odoo', password 'odoo');
-- B-2. 外部テーブル用スキーマ
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.schemata WHERE schema_name='odoo_fdw'
) THEN
EXECUTE 'CREATE SCHEMA odoo_fdw';
END IF;
END$$;
-- 既存の外部テーブルがあれば掃除
DROP FOREIGN TABLE IF EXISTS odoo_fdw.ir_model_data CASCADE;
DROP FOREIGN TABLE IF EXISTS odoo_fdw.ir_ui_view CASCADE;
-- Odoo側 public から必要テーブルのみ外部化
IMPORT FOREIGN SCHEMA public
LIMIT TO (ir_model_data, ir_ui_view)
FROM SERVER odoo_svr
INTO odoo_fdw;
-- B-3. ミラー先(devportal実体)
CREATE TABLE IF NOT EXISTS public.ir_ui_view_mirror (
id BIGINT PRIMARY KEY, -- Odooの ir_ui_view.id
xmlid TEXT UNIQUE NOT NULL, -- module.name(無ければフォールバック)
key TEXT,
name TEXT,
model TEXT,
type TEXT,
inherit_id BIGINT,
active BOOLEAN,
priority INTEGER,
arch_hash TEXT, -- md5(arch_db)
write_date TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_ir_ui_view_mirror_xmlid ON public.ir_ui_view_mirror(xmlid);
CREATE INDEX IF NOT EXISTS ix_ir_ui_view_mirror_model_type ON public.ir_ui_view_mirror(model, type);
CREATE INDEX IF NOT EXISTS ix_ir_ui_view_mirror_write_date ON public.ir_ui_view_mirror(write_date);
DROP TRIGGER IF EXISTS trg_ir_ui_view_mirror_updated_at ON public.ir_ui_view_mirror;
CREATE TRIGGER trg_ir_ui_view_mirror_updated_at
BEFORE UPDATE ON public.ir_ui_view_mirror
FOR EACH ROW EXECUTE FUNCTION public.set_timestamp_updated_at();
-- C. 初回フルロード(UPSERT)
WITH src AS (
SELECT
v.id AS id,
COALESCE(md.module || '.' || md.name, v.key, 'ir.ui.view:' || v.id::text) AS xmlid,
v.key,
v.name,
v.model,
v.type,
v.inherit_id,
v.active,
v.priority,
md5(COALESCE(v.arch_db::text, '')) AS arch_hash,
v.write_date
FROM odoo_fdw.ir_ui_view v
LEFT JOIN odoo_fdw.ir_model_data md
ON md.model = 'ir.ui.view' AND md.res_id = v.id
)
INSERT INTO public.ir_ui_view_mirror
(id, xmlid, key, name, model, type, inherit_id, active, priority, arch_hash, write_date)
SELECT
s.id, s.xmlid, s.key, s.name, s.model, s.type, s.inherit_id, s.active, s.priority, s.arch_hash, s.write_date
FROM src s
ON CONFLICT (id) DO UPDATE SET
xmlid = EXCLUDED.xmlid,
key = EXCLUDED.key,
name = EXCLUDED.name,
model = EXCLUDED.model,
type = EXCLUDED.type,
inherit_id = EXCLUDED.inherit_id,
active = EXCLUDED.active,
priority = EXCLUDED.priority,
arch_hash = EXCLUDED.arch_hash,
write_date = EXCLUDED.write_date,
updated_at = now();
-- 動作確認(必要に応じて実行)
-- SELECT count(*) FROM public.ir_ui_view_mirror;
-- SELECT id, xmlid, model, type, write_date FROM public.ir_ui_view_mirror ORDER BY id LIMIT 10;
-- (任意)誤綴りテーブルがあれば正規名にリネーム
DO $$
BEGIN
IF to_regclass('public.portal_smartbotton') IS NOT NULL
AND to_regclass('public.portal_smart_button') IS NULL THEN
EXECUTE 'ALTER TABLE public.portal_smartbotton RENAME TO portal_smart_button';
END IF;
END$$;
-- (後段)FK切替時に使用(今はコメントのまま)
-- ALTER TABLE public.portal_smart_button
-- DROP CONSTRAINT IF EXISTS portal_smart_button_view_id_fkey;
-- ALTER TABLE public.portal_smart_button
-- ADD CONSTRAINT portal_smart_button_view_id_fkey
-- FOREIGN KEY (view_id) REFERENCES public.ir_ui_view_mirror(id) ON DELETE CASCADE;
-- CREATE INDEX IF NOT EXISTS ix_portal_smart_button_view_id ON public.portal_smart_button(view_id);
コメントを残す