ent / ent

An entity framework for Go
https://entgo.io
Apache License 2.0
15.6k stars 927 forks source link

Ent generates slice fields for unique edges when using an edge schema #4167

Open krakowski opened 2 months ago

krakowski commented 2 months ago

I followed the Usage Of Edge Schema In Other Edge Types documentation for a usecase where one entity Address can have two optional edges (O2M) to entites Company and Employee. Both Company and Employee can reference 0 or 1 Address entites (constrained using an index). After running the code generator, I noticed that the fields referencing entity Address within the CompanyEdges and EmployeeEdges struct is of type slice.

I tried calling Unique on the edge builder within the Company entity, but this does not seem to be allowed when using Through.

Schema:

entc/gen: resolving edges: edge Address.companies Through("company_addresses", CompanyAddress.Type) is allowed only on M2M edges, but got: "O2M"

Current Behavior 😯

The struct field is of type []*Address

type CompanyEdges struct {
    // Address holds the value of the address edge.
    Address []*Address `json:"address,omitempty"`
}

Expected Behavior 🤔

The struct field should be of type *Address

type CompanyEdges struct {
    // Address holds the value of the address edge.
    Address *Address `json:"address,omitempty"`
}

Steps to Reproduce 🕹

  1. Clone the bug repository at https://github.com/krakowski/ent-edge-schema/tree/bug/edge-schema-types
  2. Run go test

Your Environment 🌎

Tech Version
Go 1.22.5
Ent 0.14.0
Database ---
Driver ---
a8m commented 2 months ago

Hey, @krakowski 👋

This is the expected behavior. Edge schemas are primarily meant for use in JOIN tables because extra edge fields can be attached to the type itself.

The Usage Of Edge Schema In Other Edge Types example uses a unique index to enforce the cardinality. We can consider extending this and supporting the setting of Unique edges for edge-schemas, although I'm not sure how common this is.

krakowski commented 2 months ago

Hi @a8m,

thanks for taking a look at this so fast :slightly_smiling_face:

We can consider extending this and supporting the setting of Unique edges for edge-schemas, although I'm not sure how common this is.

This would solve my problem. I convert my entities to JSON, where the mentioned fields are represented as arrays, although they can only have a single reference. In Typescript I then need to check if the array is present, has a length > 0 and extract the first value.

Generally speaking, this problem will always occur if several entities can optionally hold at most one reference to another entity.

The Address entity is a good example of this, as it can be linked to different entities (e.g Company, Warehouse, Employee, Customer, ...).

A solution to this would be a foreign key on Address within each individual entity, which can be set to NULL. However, I would like to avoid this solution as I want to avoid NULL values.

krakowski commented 2 months ago

@a8m Is there any reason why setting Unique on edges for edge-schemas is not supported / allowed? I created a code generation hook, which sets them Unique by inspecting if the conditions I mentioned in my issue are met. This way entc does not throw an error and generates the following struct (*Address instead of []*Address).

type CompanyEdges struct {
    // Address holds the value of the address edge.
    Address *Address `json:"address,omitempty"`

    // CompanyAddresses holds the value of the company_addresses edge.
    CompanyAddresses []*CompanyAddress `json:"company_addresses,omitempty"`

    // loadedTypes holds the information for reporting if a
    // type was loaded (or requested) in eager-loading or not.
    loadedTypes [2]bool
}

Here's my code generation hook.

func FixEdgeSchemaFields() gen.Hook {
  return func(next gen.Generator) gen.Generator {
    return gen.GenerateFunc(func(graph *gen.Graph) error {
      // Iterate over all nodes and edges
      for _, node := range graph.Nodes {
        for _, edge := range node.Edges {

          // Check if the current edge is connected to a edge schema
          edgeSchema := edge.Through
          if edgeSchema == nil {
            continue
          }

          // Iterate over all edges of the edge schema
          for _, edgeSchemaEdge := range edgeSchema.Edges {

            // Check if the edge schema's edge references our original node
            if edgeSchemaEdge.Type.Name == node.Name {

              // Iterate over all indexes within the edge schema
              for _, edgeSchemaIndex := range edgeSchema.Indexes {

                // Check if index contains exactly one field
                if len(edgeSchemaIndex.Columns) != 1 {
                  continue
                }

                // Compare index column name to edge schema edge's column name
                // and set the original node's edge to unique if they match
                if edgeSchemaIndex.Columns[0] == edgeSchemaEdge.Field().Name {
                  edge.Unique = true
                }
              }
            }
          }
        }
      }
      return next.Generate(graph)
    })
  }
}

EDIT

I tried running the generated code and it seems to work. Calling WithAddress fills the address field and the serialized json object doesn't contain an array field anymore. :slightly_smiling_face: