in entgo orm, is it necessary to keep the schema limited to one entity per file? whats the best practice recommendations
No, it’s not necessary to limit Ent schema files to one entity per file — but it is strongly recommended as a best practice. Here’s why:
entc
expects things.Stick with one entity per file unless you have a compelling, narrow-scoped reason to deviate. Ent’s tooling, ecosystem, and community all lean in this direction — swimming upstream isn’t worth it here.
the following is the entgo schema for an entity called abtest
package schema
import ( “time”
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/google/uuid" )
// ABTest holds holds the schema definition for the ABTest entity. type ABTest struct { ent.Schema }
// Fields of the Template. func (ABTest) Fields() []ent.Field { return []ent.Field{ field.UUID(“id”, uuid.UUID{}). Default(uuid.New). Immutable(), field.String(“name”), field.String(“description”).Optional(), field.String(“status”).Default(“draft”), //”draft”,”active”,”paused”,”completed” field.UUID(“stageId”, uuid.UUID{}).Unique(), field.UUID(“campaign_id”, uuid.UUID{}), field.String(“variants”), field.Time(“start_at”).Optional(), field.Time(“end_at”).Optional(), field.String(“success_metric”), // sign, share, donate etc field.Time(“created_at”).Default(time.Now), field.Time(“updated_at”).Default(time.Now).UpdateDefault(time.Now), } }
// Edges of the Template. func (ABTest) Edges() []ent.Edge { return []ent.Edge{ edge.From(“campaigns”, Campaign.Type).Ref(“abtests”), } }
an abtest can have multiple variants, called abtestvariant with the following schema:
package schema
import ( “time”
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/google/uuid" )
// ABTestVariant holds holds the schema definition for the ABTestVariant entity. type ABTestVariant struct { ent.Schema }
// Fields of the Template. func (ABTestVariant) Fields() []ent.Field { return []ent.Field{ field.UUID(“id”, uuid.UUID{}). Default(uuid.New). Immutable(), field.UUID(“test_id”, uuid.UUID{}). Immutable(), field.String(“name”), field.String(“description”).Optional(), field.Int(“traffic_percentage”), field.JSON(“variant_config”, map[string]any{}), field.Time(“created_at”).Default(time.Now), field.Time(“updated_at”).Default(time.Now).UpdateDefault(time.Now), } }
give me edges and indexes for both entity relations in goent
To model the relationship correctly between ABTest
and ABTestVariant
in Ent, and to optimize for performance and relational integrity, here’s what you should add:
Edge (one ABTest has many variants):
// Edges of the ABTest.
func (ABTest) Edges() []ent.Edge {
return []ent.Edge{
edge.From("campaigns", Campaign.Type).
Ref("abtests"),
edge.To("variants", ABTestVariant.Type).
StorageKey(edge.Column("test_id")),
}
}
Index (optional for query performance, depending on access patterns — e.g. querying ABTests by stageId
or campaign_id
):
func (ABTest) Indexes() []ent.Index {
return []ent.Index{
index.Fields("stageId").Unique(),
index.Fields("campaign_id"),
}
}
Edge (each variant belongs to one test):
// Edges of the ABTestVariant.
func (ABTestVariant) Edges() []ent.Edge {
return []ent.Edge{
edge.From("ab_test", ABTest.Type).
Ref("variants").
Field("test_id").
Unique().
Required(),
}
}
Index (to speed up joins and group-by on test_id
):
func (ABTestVariant) Indexes() []ent.Index {
return []ent.Index{
index.Fields("test_id"),
}
}
StorageKey
matches the field used in ABTestVariant
(test_id
), otherwise Ent will create a separate join table or get confused about foreign keys..Field().Required().Unique()
on the From
edge in ABTestVariant
ensures referential integrity without ambiguity.name
, status
, etc., based on query needs.Let me know if you’re also planning to soft-delete or version these entities — that would change indexing and defaults.
there is one more schema which is related to both test and variant, what kind of edges and indexes might this have
// Fields of the ABTestAssignment. func (ABTestAssignment) Fields() []ent.Field { return []ent.Field{ field.UUID(“id”, uuid.UUID{}). Default(uuid.New). Immutable(), field.UUID(“test_id”, uuid.UUID{}). Immutable(), field.UUID(“variant_id”, uuid.UUID{}). Immutable(), field.String(“session_id”).Optional(), field.String(“user_id”).Optional(), field.Time(“assigned_at”).Default(time.Now), } }
This ABTestAssignment
entity clearly acts as a join entity (or fact table) connecting:
ABTest
(via test_id
)ABTestVariant
(via variant_id
)You want referential integrity, query efficiency, and clear cardinality. Here’s what to add:
// Edges of the ABTestAssignment.
func (ABTestAssignment) Edges() []ent.Edge {
return []ent.Edge{
edge.From("test", ABTest.Type).
Ref("assignments").
Field("test_id").
Required().
Unique(),
edge.From("variant", ABTestVariant.Type).
Ref("assignments").
Field("variant_id").
Required().
Unique(),
}
}
If a single assignment must belong to one test and one variant, this setup is correct. If you’re planning to allow multiple assignments per test or variant, drop the .Unique().
In ABTest
:
edge.To("assignments", ABTestAssignment.Type),
In ABTestVariant
:
edge.To("assignments", ABTestAssignment.Type),
func (ABTestAssignment) Indexes() []ent.Index {
return []ent.Index{
index.Fields("test_id"),
index.Fields("variant_id"),
index.Fields("session_id"),
index.Fields("user_id"),
index.Fields("test_id", "variant_id"), // for joins / dedupes
}
}
You might also consider a composite uniqueness constraint if you want to enforce “one assignment per user/session per test”:
index.Fields("test_id", "user_id").Unique(),
Or if session_id
is primary:
index.Fields("test_id", "session_id").Unique(),
Don’t assume UUIDs are sufficient indexes on their own — join performance can degrade without explicit indexes, especially under write-heavy load.
Let me know how you plan to assign (randomly? deterministic by hash?) — it could influence schema decisions too.
set “ABTest” foreign-keys: column “test_id” definition on edge “variants” should be replaced with Field(“test_id”) on its reference “ab_test”
Yes — Ent is telling you that the foreign key column test_id must be defined on the referencing side (ABTestVariant) via .Field("test_id")
. The ABTest
schema’s edge definition should not redundantly define the column.
In ABTestVariant
:
edge.From("ab_test", ABTest.Type).
Ref("variants").
Field("test_id").
Required().
Unique(),
In ABTest
:
edge.To("variants", ABTestVariant.Type),
❗Remove the .StorageKey(edge.Column("test_id"))
from the ABTest
schema — it’s unnecessary and causes the conflict.
Do this:
.Field("test_id")
only on the From
edge (inside ABTestVariant
).To("variants", ABTestVariant.Type)
on the ABTest
sideField()
declarationThat satisfies Ent’s codegen rules and avoids duplicated foreign key definitions.
resolve “ABTestAssignment” relations: mismatch type for back-ref “variants” of ABTestAssignment.test <-> ABTest.variants
This error means Ent is confused because you’re linking ABTestAssignment.test
to ABTest.variants
, but variants
points to ABTestVariant
, not ABTestAssignment
.
You likely defined this in ABTestAssignment
:
edge.From("test", ABTest.Type).
Ref("variants"). // ❌ wrong ref
Field("test_id").
Required().
Unique(),
But variants
in ABTest
refers to ABTestVariant
, not ABTestAssignment
.
In ABTestAssignment, fix the .Ref()
target:
edge.From("test", ABTest.Type).
Ref("assignments"). // ✅ must match the edge in ABTest
Field("test_id").
Required(),
In ABTest, define the back edge:
edge.To("assignments", ABTestAssignment.Type),
Similarly, for the variant link in ABTestAssignment:
edge.From("variant", ABTestVariant.Type).
Ref("assignments").
Field("variant_id").
Required(),
And in ABTestVariant:
edge.To("assignments", ABTestAssignment.Type),
.Ref("X")
must point to a real edge.To("X", ...)
on the referenced type.Ref("variants")
instead of Ref("assignments")
) breaks codegen.