導入
Odooで見慣れた販売注文のフォーム内に並ぶ明細行は、まさにOne2manyフィールドの代表例です。親レコードに対して複数の子レコードを紐づける構造は、Odooデータモデルの核となる考え方であり、開発や設定をするなら必ず押さえておくべき概念です。
本記事は、One2manyの基本概念、Odoo ORM内部での振る舞い、実務で使うべき場面、そしてStudioやPythonコードでの作り方をわかりやすく整理して解説します。
データ構造を理解したい業務ユーザーも、実装や拡張を検討している開発者も、実務で役立つポイントを中心に学べる内容です。
OdooのOne2manyフィールドとは何か
One2manyはOdooのリレーション型フィールドの一つで、ひとつの親レコードが別モデルの複数の子レコードを参照するために使います。
イメージとしては、企業とその社員、注文とその明細、プロジェクトとそのタスクといった「親−子」の関係を表現するための仕組みです。親側で子の一覧を扱えることが利便性の源泉となります。
ユーザー画面では、One2manyはフォーム内に埋め込まれた表(インラインリスト)として表示されることが多く、親画面からそのまま子の追加・編集・削除が行えます。これがOdooのUIで非常によく使われるパターンです。
他のリレーションとの違い
Odoo ORMには代表的なリレーションが3種類あります。
- Many2one: 単一のレコードが別モデルの1レコードを参照する関係(例:受注が1顧客を指す)。
- One2many: 1つの親レコードが別モデルの複数レコードを持つ関係(例:受注が複数の受注明細を持つ)。
- Many2many: 両側が複数を参照し合う関係(例:商品とタグの関係)。
One2manyは“子レコードが一意に一つの親に所属する”場面に使います。子を複数親で共有する必要があるならMany2manyを検討すべきです。
One2manyの特徴は、親テーブルに物理的な列を持たない点です。内部的には子側のMany2oneを参照する仮想的なフィールドとして動作します。
フィールドの仕組み
データベース上は親テーブルにカラムが追加されず、子側テーブルに親を指す外部キーが保存されます。動的に子レコードを拾って表示する仕組みです。
Odooの基本ルールとして、すべてのOne2manyには対応する子モデル側のMany2oneが存在します。One2manyは逆参照(リバースルックアップ)であり、子側のMany2oneが親IDと一致する行を探して集めます。
One2manyとMany2oneの関係性
PythonでOne2manyを定義するときは、少なくとも2つのパラメータを指定します。
- comodel_name: 子レコードを保持するモデル名。
- inverse_name: 子モデル側にある親を指すMany2oneフィールド名。
実務例として、サービス契約(Service Contract)が複数の成果物(Deliverable)を持つケースを示します。
deliverable_ids = fields.One2many(
comodel_name='service.deliverable',
inverse_name='contract_id',
string='Deliverables'
)
ここで重要なのは、child側のcontract_idがMany2oneとして定義されていることです。逆にそのMany2oneが無ければOne2manyは成立しません。
データベースで起きていること
One2manyの情報は親テーブルではなく子テーブルに格納されます。各子行には親を指す外部キーカラムがあり、そこで関係が管理されます。
親レコードを表示する際、Odooは子テーブルの外部キーが親IDと一致する行を検索するSQLクエリを実行します。これは標準的なリレーショナルDBの振る舞いで、ORMが自動で処理します。
以上のことから、One2manyはデータベース列を追加しない仮想的なフィールドであり、関連レコードの“ビュー”として機能します。
ビジネスでの利用シーン
One2manyは業務上よく使われるため、現場での代表的なユースケースをいくつか挙げます。
1. 受注と受注明細
販売モジュールでは、sale.orderのorder_line_idsが受注明細(sale.order.line)を管理します。各行に商品、数量、単価、値引きなどが入り、受注フォーム上で直接行を編集できます。
2. 請求書と請求行
会計では、account.move(請求書)がOne2manyでaccount.move.line(請求行)を持ちます。請求側が子行の合計を算出して表示します。
3. プロジェクトとタスク
プロジェクトモジュールでは、project.projectのOne2manyで関連タスクを一覧表示します。フォーム内でリスト、カンバン、ガントなど異なるビュー表示が可能です。
4. 取引先と従業員・担当者
res.partnerのchild_idsは会社に紐づく個別の連絡先を列挙します。各子連絡先はMany2oneのparent_idで会社を参照します。
5. サービス/製造のカスタムモデル
独自モジュールを作る場合、メンテナンス依頼と使用部品、講座と開催日、契約と成果物のように“一つが複数を持つ”構造はOne2manyが自然な選択です。
この柔軟性が、業界ごとに最適化されたデータモデル構築でOne2manyが頻繁に使われる理由です。
フィールドの作成・カスタマイズ方法
One2manyを追加する方法は大きく分けて2通りあります。コードを書かないOdoo Studioと、直接Pythonで定義するやり方です。
Odoo Studioを使う場合
Studio単体では親モデル側からOne2manyを即座に作れない理由は、先に子側のMany2oneが存在している必要があるからです。
Studioでの推奨手順は以下の通りです。
- まず子モデルをStudioで開き、親を指すMany2oneフィールドを追加する。
- Many2oneを保存したら親モデル側に戻る。
- 親モデルに新しいフィールドを追加し、型としてOne2manyを選択。Studioから関連モデルとinverseのMany2oneを指定する。
- こうしてフォームに埋め込みリストとしてOne2manyが表示されます。
コードを書かずに関係を作れるため、業務ユーザーにはStudioが最も簡単な方法です。生成されるフィールドはPythonで定義した場合と同等に振る舞います。
Pythonコードで実装する場合
開発プロジェクトでは、PythonでOne2manyを定義すると振る舞いや命名、ドメイン制約などを細かく制御できます。以下は実例です。
from odoo import fields, models
class ServiceContract(models.Model):
_name = 'service.contract'
_description = 'Service Contract'
name = fields.Char(string='Contract Name', required=True)
deliverable_ids = fields.One2many(
comodel_name='service.deliverable',
inverse_name='contract_id',
string='Deliverables'
)
class ServiceDeliverable(models.Model):
_name = 'service.deliverable'
_description = 'Service Deliverable'
contract_id = fields.Many2one(
comodel_name='service.contract',
string='Contract',
ondelete='cascade'
)
name = fields.Char(string='Description', required=True)
ポイントは必ず子側にMany2one(ここではcontract_id)を先に定義することです。One2manyのinverse_nameはこのMany2oneの名前と厳密に一致させる必要があります。
XML-RPC API経由でフィールドを作る場合
コード管理や環境間でフィールドを自動作成する必要があるなら、ir.model.fieldsを通じてXML-RPCからOne2manyを作ることも可能です。この手法でもルールは同じで、先にMany2oneを作り、次にOne2manyでrelation_fieldにその名前を指定します。
複数環境での一括適用や自動化ワークフローに向いたやり方です。
運用上のベストプラクティス
さまざまな業界への導入経験から、One2manyを扱う際に役立つ実務的なポイントをまとめます。
- まずは必ず子側のMany2oneを作る。StudioでもコードでもAPIでも同様ですが、逆フィールドが先にないと定義できません。
- フィールド名は慣習として
_idsで終える。例:line_ids、task_idsなど。これによりコードを見たときに複数レコードを返すフィールドだと即座に分かります。 - 削除ポリシーとして
ondelete='cascade'を検討する。親が消えたときに子も自動で消す設計ならMany2oneにこのオプションを付けると、孤立レコードを防げます。 - 埋め込みリストは必要最小限の列に絞る。列が多すぎるとフォームの表示が遅くなり煩雑になります。補助的な情報は
optional属性で切り替えられるようにしましょう。 - ドメインで表示を絞る。子モデルを複数の親が共有している場合など、One2manyに
domainを付けて表示対象を制限するとユーザーの混乱を防げます。 - 大量データは一度に読み込まない。親に数千件の子があり得る場合、フォームで全部表示すると遅くなります。別画面の一覧やページングを検討してください。
よくある落とし穴
ここからは現場でよく遭遇するミスや問題点を列挙します。
逆方向フィールドを忘れる
もっとも多いエラーは、子モデルにMany2oneを作らずに親側だけでOne2manyを定義しようとして失敗するケースです。必ず子側に指定したinverse_nameが存在するか確認してください。
書き込み時のシンタックス誤り
One2manyをプログラムで更新する際は、Odoo特有のコマンドタプル構文を使う必要があります。単純なPythonリストを代入しても期待通り動きません。
(0, 0, values_dict):新しい子レコードを作る(1, child_id, values_dict):既存の子レコードを更新する(2, child_id, 0):既存の子レコードを削除する(4, child_id, 0):既存の子レコードをリレーションに追加する(削除はしない)(5, 0, 0):親から全ての子レコードの関連を外す(削除しない)
たとえばrecord.line_ids = [1, 2, 3]のような代入はORMでは正しく機能しません。必ずコマンド形式を使いましょう。
One2manyとMany2manyの混同
子を複数の親で共有する必要があるならMany2manyが適切です。One2manyで無理に実装するとレコードの重複やデータ整合性の問題が発生します。用途に応じて正しい型を選んでください。
大量行によるパフォーマンス問題
フォームに何百、何千件もの子行を表示するとページが遅くなります。会計の請求行や在庫の移動履歴でよく起きる問題です。ビューにlimitを設けるか、大きな集合は別ビューに分ける設計を検討してください。
親削除後の孤立レコード
親を削除しても子側にondelete='cascade'が設定されていないと、親参照がnullになった子レコードが残りやすくなります。時間が経つとデータベースのゴミとなり、ビューやレポートで問題を引き起こすため、削除ポリシーを明確にしましょう。
まとめ
One2manyはOdoo ORMとデータモデルの基礎を成す重要な要素です。受注明細や請求行、プロジェクトのタスクなど、プラットフォームの主要機能の多くはこの関係性に依存しています。Many2oneとの関係を理解すれば、Odoo内部の設計や拡張が格段にわかりやすくなります。
Odooを業務向けに設定する、Studioで拡張する、あるいはゼロからモジュールを作る際にも、One2manyは頻繁に用いるツールです。いつ使うか、正しい定義方法、そしてよくあるミスを避ける知識は、時間の節約とデータトラブル回避につながります。
さらに詳しいフィールドやAPI周りの解説をお探しなら、Odooの「Data & API」関連記事を順にご覧ください。
Odoo導入の支援が必要ですか?
Dasoloは企業のOdoo導入、カスタマイズ、最適化を支援します。データモデル設計やカスタムモジュールの開発、フィールド作成、既存環境の改善など、業務に合わせたソリューションを提供しています。
Odooプロジェクトに関する相談や、データ設計について議論したいことがあれば、 お気軽にお問い合わせください。喜んでサポートいたします。