Acumatica · Customization

Acumatica DAC Fundamentals — Data Access Classes Explained

A clear, beginner-friendly walk through Acumatica DACs — what they are, how they map to tables, the dual field pattern, attribute system, and the BQL types that power queries.

John Kihiu12 min read

Everything in Acumatica flows through DACs — Data Access Classes. Before you can write a useful graph extension, debug a field that won't save, or design an endpoint, you need to genuinely understand what a DAC is: not an ORM entity in the Entity Framework sense, but a metadata-rich contract that drives the database mapping, the UI, validation, defaulting, and security all at once. This post is the foundation I walk every developer through when they join one of my projects.

The shape of a DAC

A DAC is a C# class, usually mapping to one table, where every field is a pair: a property holding the value and an abstract class used to reference the field in BQL with compile-time safety.

C#
[PXCacheName("Freight Quote")]
public class FreightQuote : PXBqlTable, IBqlTable
{
    #region QuoteNbr
    [PXDBString(15, IsKey = true, IsUnicode = true)]
    [PXDefault]
    [PXUIField(DisplayName = "Quote Nbr.")]
    public virtual string QuoteNbr { get; set; }
    public abstract class quoteNbr : PX.Data.BQL.BqlString.Field<quoteNbr> { }
    #endregion

    #region Amount
    [PXDBDecimal(2)]
    [PXDefault(TypeCode.Decimal, "0.0")]
    [PXUIField(DisplayName = "Amount")]
    public virtual decimal? Amount { get; set; }
    public abstract class amount : PX.Data.BQL.BqlDecimal.Field<amount> { }
    #endregion
}

Three things to notice. Every property is nullable (decimal?, not decimal) — the framework uses null to mean "not yet defaulted," and non-nullable value types break the insert pipeline. The abstract classes (quoteNbr, amount) are what you write in BQL: Where<FreightQuote.amount, Greater<Zero>>. And the attributes are not decoration — they are the behavior.

Attributes are the behavior

Each attribute contributes handlers into the event pipeline for that field:

When a field "mysteriously" refuses to save, the answer is almost always in this list: a PXDefault without PersistingCheck.Nothing on a field the user never sees, or a selector rejecting a value inserted by code.

DACs live in caches

At runtime you never work with bare DAC rows; they live in a PXCache inside a graph. The cache tracks each row's status — Inserted, Updated, Deleted, Notchanged — and is why Acumatica can show unsaved changes, run validation before persist, and generate minimal SQL on save. The golden rule that follows: mutate rows through the cache (cache.Update(row), or assign inside an event handler where the row is already cache-managed), never by setting properties on a row you pulled from somewhere and expecting the framework to notice.

Referencing DACs in BQL and FBQL

The abstract field classes make queries type-safe. Modern fluent BQL reads close to SQL:

C#
var openQuotes = SelectFrom<FreightQuote>
    .Where<FreightQuote.amount.IsGreater<Zero>
        .And<FreightQuote.status.IsEqual<FreightQuoteStatus.open>>>
    .OrderBy<Desc<FreightQuote.amount>>
    .View.Select(this);

Because the query is built from field classes, renaming a field is a compile error everywhere it's used — one of the framework's quiet strengths compared to string-based query builders.

Keys, timestamps, and audit fields

Every persistent DAC needs a key — either IsKey = true fields forming a logical key, or a PXDBIdentity plus a human-facing key. Standard system columns come from well-known attributes: PXDBTimestamp for optimistic concurrency (skip it and last-write-wins silently), PXDBCreatedByID/PXDBLastModifiedDateTime and friends for audit, and NoteID via PXNote if the record should support notes, attachments, and tasks — which also makes it addressable by the REST API's file endpoints. When I create a custom table I add all of these by default; retrofitting concurrency after users have overwritten each other's data is a bad week.

Projection DACs

A DAC doesn't have to map one table. PXProjection DACs declare a BQL query (joins included) as their source, giving you a read-optimized, joinable "view" that still behaves as a DAC — handy for search screens and integration reads.

Wrapping up

A DAC is a contract, not a POCO: nullable properties plus abstract field classes plus attributes that collectively define storage, defaults, UI, and validation. Internalize that the attributes are executable behavior, that rows live in caches with tracked statuses, and that keys/timestamps/note IDs are non-optional plumbing on real tables — and the rest of the framework (events, extensions, BQL) starts making sense instead of feeling like folklore.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.