The Model Layer (X_ and M_ Classes)
Overview
- What you’ll learn: iDempiere’s persistence layer through the PO base class, auto-generated X_ classes, extensible M_ classes, lifecycle methods, and the Query API for database operations.
- Prerequisites: Lessons 1-16, Java programming knowledge, Eclipse development environment
- Estimated reading time: 30 minutes
Introduction
Every table in iDempiere has a corresponding Java class that handles persistence (reading from and writing to the database), business logic, and data validation. These classes form the Model Layer — the backbone of all iDempiere development. Understanding the model layer is not optional; virtually every customization, plugin, or extension you build will interact with it.
This lesson covers the three-tier class hierarchy (PO, X_, M_), the code generation process, lifecycle hooks for injecting business logic, and the Query API for database operations.
The PO (Persistent Object) Base Class
At the root of every model class is org.compiere.model.PO (Persistent Object). This abstract class provides the complete persistence framework:
What PO Does
- Database CRUD:
load(),save(),saveEx(),delete(),deleteEx() - Change tracking: Tracks which columns have been modified since the last save.
- Audit trail: Automatically manages Created, CreatedBy, Updated, UpdatedBy columns.
- Multi-tenancy: Enforces AD_Client_ID and AD_Org_ID security.
- Transaction management: Operations run within a database transaction (trxName).
- Lifecycle hooks: beforeSave(), afterSave(), beforeDelete(), afterDelete().
- Change log: Records field-level changes to the AD_ChangeLog table.
Core PO Methods
// Every model class inherits these from PO:
// Loading
PO.load(trxName) // Load record from database
PO.get_ID() // Get the primary key value
PO.get_TableName() // Get the table name
// Saving
PO.save() // Save; returns boolean
PO.saveEx() // Save; throws exception on failure (PREFERRED)
// Deleting
PO.delete(boolean force) // Delete; returns boolean
PO.deleteEx(boolean force) // Delete; throws exception on failure
// Generic field access
PO.get_Value(String columnName) // Get value as Object
PO.get_ValueAsInt(String columnName) // Get value as int
PO.get_ValueAsString(String columnName) // Get value as String
PO.get_ValueAsBoolean(String columnName) // Get value as boolean
PO.set_Value(String columnName, Object value) // Set value
// Change detection
PO.is_new() // Is this a new (unsaved) record?
PO.is_Changed() // Has any column been modified?
PO.is_ValueChanged(String columnName) // Has this specific column changed?
PO.get_ValueOld(String columnName) // Get the previous value
// Context and transaction
PO.getCtx() // Get the Properties context
PO.get_TrxName() // Get the current transaction name
PO.getAD_Client_ID() // Get the Client ID
PO.getAD_Org_ID() // Get the Organization ID
X_ Classes: The Generated Layer
For every table registered in the Application Dictionary, iDempiere can generate an X_ class. These classes are auto-generated and provide type-safe getters and setters for every column.
What Gets Generated
Given a table C_Order with columns like DocumentNo, GrandTotal, C_BPartner_ID, the generator creates:
// Auto-generated: X_C_Order.java
public class X_C_Order extends PO implements I_C_Order {
/** Table ID */
public static final int Table_ID = 259;
/** Table Name */
public static final String Table_Name = "C_Order";
// Standard constructors
public X_C_Order(Properties ctx, int C_Order_ID, String trxName) {
super(ctx, C_Order_ID, trxName);
}
public X_C_Order(Properties ctx, ResultSet rs, String trxName) {
super(ctx, rs, trxName);
}
// Type-safe getter and setter for DocumentNo (String column)
public void setDocumentNo(String DocumentNo) {
set_Value(COLUMNNAME_DocumentNo, DocumentNo);
}
public String getDocumentNo() {
return (String) get_Value(COLUMNNAME_DocumentNo);
}
// Type-safe getter and setter for GrandTotal (BigDecimal column)
public void setGrandTotal(BigDecimal GrandTotal) {
set_Value(COLUMNNAME_GrandTotal, GrandTotal);
}
public BigDecimal getGrandTotal() {
BigDecimal bd = (BigDecimal) get_Value(COLUMNNAME_GrandTotal);
if (bd == null) return Env.ZERO;
return bd;
}
// Foreign key: C_BPartner_ID
public void setC_BPartner_ID(int C_BPartner_ID) {
if (C_BPartner_ID < 1)
set_Value(COLUMNNAME_C_BPartner_ID, null);
else
set_Value(COLUMNNAME_C_BPartner_ID, C_BPartner_ID);
}
public int getC_BPartner_ID() {
return get_ValueAsInt(COLUMNNAME_C_BPartner_ID);
}
// Column name constants
public static final String COLUMNNAME_DocumentNo = "DocumentNo";
public static final String COLUMNNAME_GrandTotal = "GrandTotal";
public static final String COLUMNNAME_C_BPartner_ID = "C_BPartner_ID";
// ... more columns
}
Key Points About X_ Classes
- Never edit X_ classes manually. They are regenerated when columns change, and your edits will be overwritten.
- They extend
POdirectly and implement the correspondingI_interface. - They provide null-safe getters (numeric types return 0 or Env.ZERO instead of null).
- They define column name constants (COLUMNNAME_xxx) used throughout the codebase.
- Each class has two constructors: one that loads by ID and one that loads from a ResultSet.
M_ Classes: The Business Logic Layer
M_ classes extend the corresponding X_ class and contain hand-written business logic. This is where developers add validations, calculations, document processing, and custom behavior.
// Hand-written: MOrder.java
public class MOrder extends X_C_Order implements DocAction {
// Constructor delegates to generated class
public MOrder(Properties ctx, int C_Order_ID, String trxName) {
super(ctx, C_Order_ID, trxName);
}
// Business logic: validate before saving
@Override
protected boolean beforeSave(boolean newRecord) {
// Validate that the business partner is active
MBPartner bp = MBPartner.get(getCtx(), getC_BPartner_ID());
if (bp == null || !bp.isActive()) {
log.saveError("Error", "Business Partner is not active");
return false;
}
// Auto-set the warehouse from the organization if not set
if (getM_Warehouse_ID() == 0) {
MOrgInfo orgInfo = MOrgInfo.get(getCtx(),
getAD_Org_ID(), get_TrxName());
if (orgInfo.getM_Warehouse_ID() > 0) {
setM_Warehouse_ID(orgInfo.getM_Warehouse_ID());
}
}
return true; // Allow save to proceed
}
// Business logic: actions after saving
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
if (!success) return false;
// If the business partner changed, update related records
if (is_ValueChanged(COLUMNNAME_C_BPartner_ID)) {
updateOrderLines();
}
return true;
}
// Custom business methods
public MOrderLine[] getLines() {
// Return order lines for this order
return getLines(false, null);
}
public BigDecimal calculateTotalWeight() {
BigDecimal weight = Env.ZERO;
for (MOrderLine line : getLines()) {
MProduct product = line.getProduct();
if (product != null && product.getWeight() != null) {
weight = weight.add(
product.getWeight().multiply(line.getQtyOrdered()));
}
}
return weight;
}
// ... document processing methods (prepareIt, completeIt, etc.)
}
The Relationship: PO → X_ → M_
PO (abstract)
└── X_C_Order (generated: getters, setters, column constants)
└── MOrder (hand-written: business logic, validations, processing)
PO (abstract)
└── X_C_Invoice (generated)
└── MInvoice (hand-written)
PO (abstract)
└── X_C_Payment (generated)
└── MPayment (hand-written)
The Model Generation Process
When you add or modify columns in the Application Dictionary, you need to regenerate the X_ and I_ classes. iDempiere includes a built-in tool for this.
Using the Generate Model Tool
- In iDempiere, navigate to Application Dictionary > Generate Model (it is a process, not a window).
- Configure the parameters:
- Directory: The source directory path (e.g.,
/path/to/idempiere/org.adempiere.base/src/). - Package: The Java package (e.g.,
org.compiere.model). - Table: Select the specific table, or leave blank to regenerate all.
- EntityType: Filter by entity type (e.g., “D” for dictionary, “U” for user).
- Directory: The source directory path (e.g.,
- Run the process. It generates/regenerates the
X_andI_files.
What Gets Generated vs. What You Write
Generated (DO NOT EDIT):
I_C_Order.java -- Interface with method signatures
X_C_Order.java -- Implementation with getters/setters
Hand-written (YOUR CODE):
MOrder.java -- Business logic extending X_C_Order
Lifecycle Methods
The lifecycle methods are the primary extension points in the model layer. PO calls them at specific moments during the persistence lifecycle, and M_ classes override them to inject business logic.
beforeSave(boolean newRecord)
Called before a record is written to the database. Use it for validation and data preparation.
@Override
protected boolean beforeSave(boolean newRecord) {
// Parameter: newRecord is true for INSERT, false for UPDATE
// Validate business rules
if (getQtyOrdered().signum() <= 0) {
log.saveError("Error", "Quantity must be positive");
return false; // Prevents save
}
// Calculate derived values
setLineNetAmt(getQtyOrdered().multiply(getPriceActual()));
// Set defaults for new records
if (newRecord) {
if (getDocStatus() == null) {
setDocStatus(DOCSTATUS_Drafted);
}
}
return true; // Allow save
}
afterSave(boolean newRecord, boolean success)
Called after the record is written to the database (but still within the transaction). Use it for cascading updates and related record creation.
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
if (!success) return false; // Always check this first
// Update parent record's total when a line is saved
if (is_ValueChanged(COLUMNNAME_LineNetAmt) || newRecord) {
MOrder order = new MOrder(getCtx(), getC_Order_ID(), get_TrxName());
order.updateTotalLines();
order.saveEx();
}
// Create default sub-records for new records
if (newRecord) {
createDefaultTaxLine();
}
return true;
}
beforeDelete()
Called before a record is deleted. Use it to prevent deletion under certain conditions or to clean up related records.
@Override
protected boolean beforeDelete() {
// Prevent deletion of completed documents
if (DOCSTATUS_Completed.equals(getDocStatus())) {
log.saveError("Error", "Cannot delete completed documents");
return false;
}
// Delete child records first (if cascading is needed)
for (MOrderLine line : getLines()) {
line.deleteEx(true);
}
return true;
}
afterDelete(boolean success)
Called after deletion. Use it for cleanup operations that depend on the record being removed.
@Override
protected boolean afterDelete(boolean success) {
if (!success) return false;
// Update parent totals after line deletion
MOrder order = new MOrder(getCtx(), getC_Order_ID(), get_TrxName());
order.updateTotalLines();
order.saveEx();
return true;
}
Creating, Saving, and Deleting Records Programmatically
Creating a New Record
// Create a new Business Partner
MBPartner bp = new MBPartner(ctx, 0, trxName); // ID=0 means new record
bp.setValue("VENDOR001");
bp.setName("Acme Supplies");
bp.setIsVendor(true);
bp.setIsCustomer(false);
bp.setC_BP_Group_ID(1000000); // Business Partner Group
bp.saveEx(); // Throws AdempiereException on failure
int newId = bp.getC_BPartner_ID(); // Get the auto-generated ID
log.info("Created BPartner with ID: " + newId);
Loading and Updating an Existing Record
// Load an existing order by ID
MOrder order = new MOrder(ctx, 1000123, trxName);
// Verify it loaded (ID > 0 means it exists)
if (order.get_ID() == 0) {
throw new AdempiereException("Order not found");
}
// Modify fields
order.setDescription("Updated via code");
order.setPriorityRule(MOrder.PRIORITYRULE_High);
order.saveEx();
Deleting a Record
// Load and delete
MOrderLine line = new MOrderLine(ctx, lineId, trxName);
line.deleteEx(true); // force=true means delete even with dependencies
// Or use the boolean version if you want to handle failure gracefully
boolean deleted = line.delete(true);
if (!deleted) {
log.warning("Failed to delete order line: " + lineId);
}
MTable.get() for Dynamic Table Access
When you do not know the table at compile time, use MTable to create PO instances dynamically:
// Get the table definition
MTable table = MTable.get(ctx, "C_Order");
// Load a record by ID
PO record = table.getPO(1000123, trxName);
// Access fields generically
String docNo = record.get_ValueAsString("DocumentNo");
int bpId = record.get_ValueAsInt("C_BPartner_ID");
// Create a new record
PO newRecord = table.getPO(0, trxName);
newRecord.set_Value("DocumentNo", "NEW-001");
newRecord.saveEx();
The Query Class
The Query class provides a fluent API for building database queries without writing raw SQL. It is type-safe, injection-resistant, and the recommended way to query data in iDempiere.
Basic Query Examples
import org.compiere.model.Query;
// Find all active orders for a specific business partner
List<MOrder> orders = new Query(ctx, MOrder.Table_Name,
"C_BPartner_ID=? AND IsActive='Y'", trxName)
.setParameters(bpartnerId)
.setOrderBy("DateOrdered DESC")
.list();
// Find a single record
MOrder order = new Query(ctx, MOrder.Table_Name,
"DocumentNo=?", trxName)
.setParameters("SO-001234")
.setClient_ID() // Automatically filter by client
.firstOnly(); // Returns null if not found,
// throws if multiple found
// Count records
int count = new Query(ctx, MOrder.Table_Name,
"DocStatus=? AND DateOrdered>=?", trxName)
.setParameters(MOrder.DOCSTATUS_Completed, startDate)
.count();
// Get just the IDs (more efficient when you don't need full objects)
int[] orderIds = new Query(ctx, MOrder.Table_Name,
"C_BPartner_ID=?", trxName)
.setParameters(bpartnerId)
.getIDs();
Advanced Query Features
// Aggregate query
BigDecimal totalSales = new Query(ctx, MOrder.Table_Name,
"IsSOTrx='Y' AND DocStatus IN ('CO','CL')", trxName)
.setClient_ID()
.aggregate("GrandTotal", Query.AGGREGATE_SUM);
// First with specific ordering
MOrderLine cheapestLine = new Query(ctx, MOrderLine.Table_Name,
"C_Order_ID=?", trxName)
.setParameters(orderId)
.setOrderBy("PriceActual ASC")
.first();
// Iterate over large result sets efficiently
new Query(ctx, MOrder.Table_Name,
"DocStatus=?", trxName)
.setParameters(MOrder.DOCSTATUS_Drafted)
.setClient_ID()
.forEach(order -> {
// Process each order without loading all into memory
processOrder((MOrder) order);
});
Understanding the I_ Interfaces
For every table, the generator also creates an I_ interface (e.g., I_C_Order). These interfaces define the public contract of the model class:
// Generated: I_C_Order.java
public interface I_C_Order {
public int getC_Order_ID();
public String getDocumentNo();
public void setDocumentNo(String DocumentNo);
public BigDecimal getGrandTotal();
public void setGrandTotal(BigDecimal GrandTotal);
public int getC_BPartner_ID();
public I_C_BPartner getC_BPartner() throws RuntimeException;
// ... all column accessors
}
The interfaces are useful for type-safe programming when you work with generic PO references. They also define navigation methods for foreign keys — notice getC_BPartner() returns the related I_C_BPartner object, not just the ID.
ModelFactory: How iDempiere Resolves Classes
When iDempiere needs to instantiate a model object, it does not simply call new X_C_Order(). Instead, it uses the ModelFactory pattern to resolve the correct class:
- The framework receives a table name and record ID.
- It queries all registered
IModelFactoryimplementations (via OSGi services). - Each factory is asked: “Can you create an instance for this table?”
- The default factory checks for an M_ class first. If
MOrderexists, it creates anMOrderinstance. - If no M_ class exists, it falls back to the X_ class.
- If no X_ class exists, it creates a generic
GenericPOinstance.
// This is why MTable.getPO() returns MOrder, not X_C_Order:
PO record = MTable.get(ctx, "C_Order").getPO(orderId, trxName);
// record is actually an MOrder instance
// You can verify:
log.info(record.getClass().getName());
// Output: org.compiere.model.MOrder
Custom ModelFactory for Plugins
Plugins can register their own ModelFactory to override the default class resolution. This allows a plugin to substitute its own model class for a core table:
public class MyModelFactory implements IModelFactory {
@Override
public Class<?> getClass(String tableName) {
if ("C_Order".equals(tableName)) {
return MyCustomOrder.class; // Your extended MOrder
}
return null; // Let other factories handle it
}
@Override
public PO getPO(String tableName, int Record_ID,
String trxName) {
if ("C_Order".equals(tableName)) {
return new MyCustomOrder(Env.getCtx(), Record_ID, trxName);
}
return null;
}
}
Practical Example: Building a Custom Model Class
Let us put it all together by building a model class for a custom Expense Report table:
// Step 1: Generated X_ class (created by Generate Model tool)
// X_Z_ExpenseReport.java — DO NOT EDIT
public class X_Z_ExpenseReport extends PO implements I_Z_ExpenseReport {
public static final int Table_ID = 1000100;
public static final String Table_Name = "Z_ExpenseReport";
public X_Z_ExpenseReport(Properties ctx, int Z_ExpenseReport_ID,
String trxName) {
super(ctx, Z_ExpenseReport_ID, trxName);
}
// Generated getters/setters...
public void setDescription(String Description) {
set_Value("Description", Description);
}
public String getDescription() {
return (String) get_Value("Description");
}
public void setTotalAmt(BigDecimal TotalAmt) {
set_Value("TotalAmt", TotalAmt);
}
public BigDecimal getTotalAmt() {
BigDecimal bd = (BigDecimal) get_Value("TotalAmt");
if (bd == null) return Env.ZERO;
return bd;
}
// ... more generated code
}
// Step 2: Hand-written M_ class — YOUR BUSINESS LOGIC
public class MZExpenseReport extends X_Z_ExpenseReport {
private static final CLogger log =
CLogger.getCLogger(MZExpenseReport.class);
public MZExpenseReport(Properties ctx, int id, String trxName) {
super(ctx, id, trxName);
}
public MZExpenseReport(Properties ctx, ResultSet rs, String trxName) {
super(ctx, rs, trxName);
}
/** Get all expense lines for this report */
public MZExpenseLine[] getLines() {
List<MZExpenseLine> lines = new Query(getCtx(),
MZExpenseLine.Table_Name,
"Z_ExpenseReport_ID=?", get_TrxName())
.setParameters(getZ_ExpenseReport_ID())
.setOrderBy("Line")
.list();
return lines.toArray(new MZExpenseLine[0]);
}
/** Recalculate the total from lines */
public void updateTotalAmt() {
BigDecimal total = new Query(getCtx(),
MZExpenseLine.Table_Name,
"Z_ExpenseReport_ID=?", get_TrxName())
.setParameters(getZ_ExpenseReport_ID())
.aggregate("Amt", Query.AGGREGATE_SUM);
if (total == null) total = Env.ZERO;
setTotalAmt(total);
}
@Override
protected boolean beforeSave(boolean newRecord) {
// Validate: description is required
if (getDescription() == null
|| getDescription().trim().isEmpty()) {
log.saveError("FillMandatory", "Description");
return false;
}
// Recalculate total before saving
updateTotalAmt();
return true;
}
@Override
protected boolean afterSave(boolean newRecord, boolean success) {
if (!success) return false;
// Log creation of new expense reports
if (newRecord) {
log.info("New expense report created: "
+ getDocumentNo() + " Total: " + getTotalAmt());
}
return true;
}
@Override
protected boolean beforeDelete() {
// Only allow deletion of draft reports
if (!"DR".equals(getDocStatus())) {
log.saveError("Error",
"Only draft expense reports can be deleted");
return false;
}
return true;
}
/** Static helper: find reports pending approval */
public static List<MZExpenseReport> getPendingApproval(
Properties ctx, String trxName) {
return new Query(ctx, Table_Name,
"DocStatus='DR' AND IsApproved='N'", trxName)
.setClient_ID()
.setOrderBy("Created")
.list();
}
}
Key Takeaways
- The model layer has three tiers: PO (persistence base), X_ (generated getters/setters), M_ (hand-written business logic).
- Never edit X_ classes — they are regenerated. All custom logic goes in M_ classes.
- Use lifecycle methods (beforeSave, afterSave, beforeDelete, afterDelete) to inject business rules at the persistence level.
- Always use
saveEx()anddeleteEx()(the exception-throwing variants) in application code. - The Query class provides a fluent, type-safe API for database queries — prefer it over raw SQL.
- MTable.get().getPO() dynamically instantiates the correct M_ class thanks to the ModelFactory pattern.
- Plugins can register custom IModelFactory implementations to override default class resolution.
- The I_ interfaces define the public contract and enable type-safe access to related records via navigation methods.
What’s Next
With a solid understanding of the model layer, you are ready to explore more advanced development topics. In the next lessons, you will learn about Model Validators (event-driven hooks that complement lifecycle methods), building OSGi plugins, and creating custom processes and reports that leverage the model layer you now understand.
繁體中文
概述
- 學習內容:透過 PO 基礎類別、自動產生的 X_ 類別、可擴展的 M_ 類別、生命週期方法和資料庫操作的 Query API 了解 iDempiere 的持久層。
- 先修條件:第 1-16 課,Java 程式設計知識,Eclipse 開發環境
- 預估閱讀時間:30 分鐘
簡介
iDempiere 中的每個資料表都有對應的 Java 類別,負責處理持久化(從資料庫讀取和寫入)、業務邏輯和資料驗證。這些類別構成模型層——所有 iDempiere 開發的基礎。了解模型層不是可選的;幾乎您建立的每個自訂、Plugin 或擴展都會與它互動。
本課涵蓋三層類別階層(PO、X_、M_)、程式碼產生流程、用於注入業務邏輯的生命週期勾子,以及資料庫操作的 Query API。
PO(Persistent Object)基礎類別
每個模型類別的根是 org.compiere.model.PO(Persistent Object)。這個抽象類別提供完整的持久化框架:
- 資料庫 CRUD:
load()、save()、saveEx()、delete()、deleteEx() - 變更追蹤:追蹤自上次儲存以來哪些欄位已被修改。
- 稽核追蹤:自動管理 Created、CreatedBy、Updated、UpdatedBy 欄位。
- 多租戶:強制執行 AD_Client_ID 和 AD_Org_ID 安全性。
- 交易管理:操作在資料庫交易(trxName)內執行。
- 生命週期勾子:beforeSave()、afterSave()、beforeDelete()、afterDelete()。
- 變更日誌:將欄位層級的變更記錄到 AD_ChangeLog 資料表。
X_ 類別:產生層
對於在應用程式字典中註冊的每個資料表,iDempiere 可以產生一個 X_ 類別。這些類別是自動產生的,為每個欄位提供類型安全的 getter 和 setter。
關於 X_ 類別的要點:
- 切勿手動編輯 X_ 類別。當欄位更改時會重新產生,您的修改將被覆蓋。
- 它們直接繼承
PO並實作對應的I_介面。 - 它們提供空值安全的 getter(數值類型返回 0 或 Env.ZERO 而非 null)。
- 它們定義在整個程式碼庫中使用的欄位名稱常數(COLUMNNAME_xxx)。
M_ 類別:業務邏輯層
M_ 類別繼承對應的 X_ 類別並包含手動撰寫的業務邏輯。這是開發者新增驗證、計算、文件處理和自訂行為的地方。
關係:PO → X_ → M_
PO(抽象)
└── X_C_Order(產生的:getter、setter、欄位常數)
└── MOrder(手動撰寫:業務邏輯、驗證、處理)
模型產生流程
當您在應用程式字典中新增或修改欄位時,需要重新產生 X_ 和 I_ 類別。iDempiere 包含一個內建工具。導覽至應用程式字典 > Generate Model,配置目錄、套件、資料表和 EntityType 參數,然後執行流程。
生命週期方法
生命週期方法是模型層中的主要擴展點。PO 在持久化生命週期的特定時刻呼叫它們,M_ 類別覆寫它們以注入業務邏輯。
beforeSave(boolean newRecord)
在記錄寫入資料庫之前呼叫。用於驗證和資料準備。返回 false 可防止儲存。
afterSave(boolean newRecord, boolean success)
在記錄寫入資料庫之後呼叫(但仍在交易內)。用於級聯更新和相關記錄建立。
beforeDelete()
在記錄被刪除之前呼叫。用於防止特定條件下的刪除或清理相關記錄。
afterDelete(boolean success)
在刪除之後呼叫。用於依賴記錄移除的清理操作。
以程式方式建立、儲存和刪除記錄
建立新記錄時使用 ID=0 的建構子,修改後呼叫 saveEx()。載入現有記錄時使用記錄 ID 的建構子。刪除記錄時使用 deleteEx(true)。
MTable.get() 用於動態資料表存取
當您在編譯時不知道資料表時,使用 MTable 動態建立 PO 實例。
Query 類別
Query 類別提供流暢的 API 來建構資料庫查詢,無需撰寫原始 SQL。它是類型安全的、防注入的,也是 iDempiere 中查詢資料的推薦方式。
// 查詢特定業務夥伴的所有作用中訂單
List<MOrder> orders = new Query(ctx, MOrder.Table_Name,
"C_BPartner_ID=? AND IsActive='Y'", trxName)
.setParameters(bpartnerId)
.setOrderBy("DateOrdered DESC")
.list();
了解 I_ 介面
對於每個資料表,產生器還會建立一個 I_ 介面。這些介面定義模型類別的公共契約,對於類型安全程式設計和外鍵的導覽方法很有用。
ModelFactory:iDempiere 如何解析類別
當 iDempiere 需要實例化模型物件時,它使用 ModelFactory 模式來解析正確的類別。框架首先查找 M_ 類別,如果不存在則回退到 X_ 類別,如果也不存在則建立通用的 GenericPO 實例。Plugin 可以註冊自己的 ModelFactory 來覆寫預設的類別解析。
重點摘要
- 模型層有三層:PO(持久化基礎)、X_(產生的 getter/setter)、M_(手動撰寫的業務邏輯)。
- 切勿編輯 X_ 類別——它們會被重新產生。所有自訂邏輯都放在 M_ 類別中。
- 使用生命週期方法(beforeSave、afterSave、beforeDelete、afterDelete)在持久層注入業務規則。
- 在應用程式碼中始終使用
saveEx()和deleteEx()(拋出例外的變體)。 - Query 類別提供流暢、類型安全的資料庫查詢 API——優先使用而非原始 SQL。
- MTable.get().getPO() 透過 ModelFactory 模式動態實例化正確的 M_ 類別。
- Plugin 可以註冊自訂 IModelFactory 實作來覆寫預設的類別解析。
- I_ 介面定義公共契約並透過導覽方法啟用對相關記錄的類型安全存取。
下一步
有了對模型層的紮實了解,您已準備好探索更進階的開發主題。在接下來的課程中,您將學習 Model Validator(補充生命週期方法的事件驅動勾子)、建立 OSGi Plugin,以及建立利用您現在了解的模型層的自訂流程和報表。
日本語
概要
- 学習内容:PO基底クラス、自動生成されたX_クラス、拡張可能なM_クラス、ライフサイクルメソッド、データベース操作のQuery APIを通じたiDempiereの永続化レイヤーの理解。
- 前提条件:レッスン1〜16、Javaプログラミング知識、Eclipse開発環境
- 推定読了時間:30分
はじめに
iDempiereのすべてのテーブルには、永続化(データベースの読み書き)、ビジネスロジック、データバリデーションを処理する対応するJavaクラスがあります。これらのクラスがモデルレイヤーを構成し、すべてのiDempiere開発の基盤です。モデルレイヤーの理解はオプションではありません。構築するほぼすべてのカスタマイズ、プラグイン、拡張がこれと対話します。
このレッスンでは、3層のクラス階層(PO、X_、M_)、コード生成プロセス、ビジネスロジックを注入するためのライフサイクルフック、データベース操作のQuery APIを扱います。
PO(Persistent Object)基底クラス
すべてのモデルクラスのルートはorg.compiere.model.POです。この抽象クラスは完全な永続化フレームワークを提供します:
- データベースCRUD:
load()、save()、saveEx()、delete()、deleteEx() - 変更追跡:最後の保存以降に変更されたカラムを追跡。
- 監査証跡:Created、CreatedBy、Updated、UpdatedByカラムを自動管理。
- マルチテナンシー:AD_Client_IDとAD_Org_IDのセキュリティを強制。
- トランザクション管理:操作はデータベーストランザクション(trxName)内で実行。
- ライフサイクルフック:beforeSave()、afterSave()、beforeDelete()、afterDelete()。
X_クラス:生成レイヤー
アプリケーション辞書に登録されたすべてのテーブルに対して、iDempiereはX_クラスを生成できます。これらは自動生成され、すべてのカラムに型安全なgetterとsetterを提供します。
- X_クラスを手動で編集しないでください。カラムが変更されると再生成され、編集は上書きされます。
POを直接継承し、対応するI_インターフェースを実装します。- null安全なgetter(数値型はnullの代わりに0またはEnv.ZEROを返す)を提供。
M_クラス:ビジネスロジックレイヤー
M_クラスは対応するX_クラスを継承し、手書きのビジネスロジックを含みます。開発者がバリデーション、計算、ドキュメント処理、カスタム動作を追加する場所です。
関係:PO → X_ → M_
PO(抽象)
└── X_C_Order(生成:getter、setter、カラム定数)
└── MOrder(手書き:ビジネスロジック、バリデーション、処理)
モデル生成プロセス
アプリケーション辞書でカラムを追加または変更した場合、X_とI_クラスを再生成する必要があります。アプリケーション辞書 > Generate Modelでプロセスを実行します。
ライフサイクルメソッド
ライフサイクルメソッドはモデルレイヤーの主要な拡張ポイントです。
beforeSave(boolean newRecord)
レコードがデータベースに書き込まれる前に呼び出されます。バリデーションとデータ準備に使用。falseを返すと保存を防止。
afterSave(boolean newRecord, boolean success)
レコードがデータベースに書き込まれた後(トランザクション内)に呼び出されます。カスケード更新と関連レコードの作成に使用。
beforeDelete()
レコードが削除される前に呼び出されます。特定の条件下での削除防止や関連レコードのクリーンアップに使用。
afterDelete(boolean success)
削除後に呼び出されます。レコード削除に依存するクリーンアップ操作に使用。
プログラムによるレコードの作成、保存、削除
新しいレコードを作成するにはID=0のコンストラクタを使用し、変更後にsaveEx()を呼び出します。既存のレコードをロードするにはレコードIDのコンストラクタを使用します。削除にはdeleteEx(true)を使用します。
MTable.get()による動的テーブルアクセス
コンパイル時にテーブルがわからない場合、MTableを使用してPOインスタンスを動的に作成します。
Queryクラス
Queryクラスは、生のSQLを書くことなくデータベースクエリを構築するための流暢なAPIを提供します。型安全でインジェクション耐性があり、iDempiereでデータをクエリする推奨方法です。
// 特定の取引先のすべてのアクティブな注文を検索
List<MOrder> orders = new Query(ctx, MOrder.Table_Name,
"C_BPartner_ID=? AND IsActive='Y'", trxName)
.setParameters(bpartnerId)
.setOrderBy("DateOrdered DESC")
.list();
I_インターフェースの理解
すべてのテーブルに対して、ジェネレーターはI_インターフェースも作成します。これらはモデルクラスのパブリックコントラクトを定義し、外部キーのナビゲーションメソッドを提供します。
ModelFactory:iDempiereのクラス解決方法
iDempiereがモデルオブジェクトをインスタンス化する必要がある場合、ModelFactoryパターンを使用して正しいクラスを解決します。まずM_クラスを探し、なければX_クラスにフォールバック、それもなければ汎用のGenericPOインスタンスを作成します。プラグインは独自のModelFactoryを登録してデフォルトのクラス解決をオーバーライドできます。
重要ポイント
- モデルレイヤーは3層:PO(永続化基盤)、X_(生成されたgetter/setter)、M_(手書きのビジネスロジック)。
- X_クラスは編集しない——再生成されます。すべてのカスタムロジックはM_クラスに。
- ライフサイクルメソッド(beforeSave、afterSave、beforeDelete、afterDelete)で永続化レベルにビジネスルールを注入。
- アプリケーションコードでは常に
saveEx()とdeleteEx()を使用。 - Queryクラスは流暢で型安全なデータベースクエリAPI——生SQLより推奨。
- MTable.get().getPO()はModelFactoryパターンにより正しいM_クラスを動的にインスタンス化。
- プラグインはカスタムIModelFactory実装を登録してデフォルトのクラス解決をオーバーライド可能。
- I_インターフェースはパブリックコントラクトを定義し、ナビゲーションメソッドで関連レコードへの型安全なアクセスを提供。
次のステップ
モデルレイヤーをしっかり理解したので、より高度な開発トピックを探索する準備ができました。次のレッスンでは、Model Validator、OSGiプラグインの構築、モデルレイヤーを活用したカスタムプロセスとレポートの作成について学びます。