/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } M7スマートボタン実装 – Raqqa

M7スマートボタン実装

。**「Odoo本番DBの ir_ui_view を devportal 側へミラー → portal_smart_button.view_id をミラー表にFK」**に寄せると、将来の整合性・保守性が一気に楽になります。

ここではコードは出さずに、やり方・判断ポイント・切替手順を“作業手順書”としてまとめます。
(前段の 422 や OpenAPI/ルータの混線は最後にチェックリスト化)


ミラーリングの選択肢(結論→おすすめ順)

  1. 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 / 手動)で反映。運用・責務が明快。
  1. 論理レプリケーション(Publication/Subscription)
  • odoo 側で ir_ui_view / ir_model_data公開、devportal 側で購読しローカル表へ自動反映。
  • ほぼリアルタイムだが、**権限(superuser 相当)**や接続要件がFDWより重い。運用の難易度◎→上級向け。
  1. ETLバッチ(Python or psql)
  • アプリやCronJobで直クエリ→Upsert。自由度高いがメンテ工数が乗る。
  • まずは FDW で安定運用→必要なら ETL に移行、が無難。

あなたの現在構成(同一Postgresインスタンス内に odoodevportal が同居)だと、FDWが一番シンプルで堅いです。


FDW 方式:手順(ノーコード版の運用手順書)

A. 事前設計

  • 接続元/先
    • 接続元:devportal DB(ミラーを置く場所)
    • 接続先:odoo DB(ソース:ir_ui_view / ir_model_data
  • 権限
    • odoo側に読み取り専用ユーザSELECT on ir_ui_view, ir_model_data)。
    • devportal側に拡張作成権限(CREATE EXTENSION postgres_fdw)。
  • ミラー表の項目(最低限)
    • 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_hasharch_dbのハッシュ)を持たせると差分検出が高速化
    • 監査:created_at, updated_at(自動更新トリガ)

xmlid の作り方ir_model_datamodel='ir.ui.view' AND res_id=view.id を結合し、module || '.' || name を採用。無い場合は ir_ui_view.key をフォールバック。

B. 一度きりの初期設定

  1. FDW有効化(devportal 側で postgres_fdw を有効に)
  2. 外部サーバ定義(devportal→odoo への接続・DB名・ホスト・ポート)
  3. ユーザマッピング(読み取りユーザ+パスの関連付け)
  4. 外部テーブル定義 or スキーマインポート
    • 最低限 ir_ui_viewir_model_data を外部化
  5. ローカル鏡テーブル作成public.ir_ui_view_mirror
    • PK=idxmlid UNIQUE、必要な補助Index(write_date など)

C. 初回フルロード

  • 外部表の ir_ui_viewir_model_dataJOINし、鏡テーブルへ一括投入
  • xmlid 重複は基本ないが、ユニーク制約違反はログし、人手で解決(最初に品質を揃えるのが肝)。

D. 差分反映(選択肢)

  • 方法D-1:write_date 増分
    • devportal 側に最後の同期時刻を保持し、write_date > last_sync の行のみUpsert。
    • **削除(odoo 側で view 削除)**も扱うなら、
      • a) フル比較(遅いが確実)、または
      • b) odoo 側の削除履歴(無ければソフトデリート扱いでよしとする方針定義)。
  • 方法D-2:arch_hash 比較
    • 内容差分のみ再取り込み。write_date が緩い運用でも正確。
  • 方法D-3:夜間フルREFRESH
    • 件数が大きくなければ、夜間に全件入れ替え。簡単・堅牢。
    • その場合は二相テーブル_stagingSWAP)で無停止切り替え。

スタート時は 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. 鏡テーブルが安定(初回フル+差分1回以上)してから進める。
  2. 既存の portal_smart_button.view_idportal_view(id) を指しているなら、移行マッピングが必要:
    • portal_view にも xmlid が持てるなら、
      1. portal_view.xmlid鏡の ir_ui_view_mirror.xmlid で突き合わせ
      2. portal_smart_button.view_idmirror の id更新(移行スクリプト)
    • xmlid を持っていないなら、model + type + name 等から推定し、最終的に人手確認
  3. すべての portal_smart_button.view_idmirror 側の id になったら、
    • 旧FK(portal_view 向き)を削除
    • 新FK(ir_ui_view_mirror(id) 向き)を追加(ON DELETE CASCADE)。
  4. 一斉テスト
    • POST /smart_buttonsview_xmlid のみ)で作成 → サーバ側でmirror解決→201
    • GET /smart_buttons で返る view_idmirrorのidであることを確認
    • 代表的な view で存在しないxmlidや重複xmlid時のエラーメッセージを確認(422系の語彙統一)。
  5. OpenAPI 説明の明確化
    • view_idOdooの view.id(=mirror.id) であることを記載
    • view_xmlidmodule.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 --overwritekubectl set imagerollout status
    • ブラウザはキャッシュ無効化+ハードリロードで確認。
  • 翻訳の対象拡張
    • label_i18n に加えて notes_i18nonly_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);

Comments

コメントを残す

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