Skip to content

ChildMods.WithoutParent does nothing, factory generates a new parent entity for every associated child #510

@abdusco

Description

@abdusco

The factory generates a new parent entity for every associated child, causing many parent records to be created. To illustrate the issue, imagine we have this schema:

CREATE TABLE orders
(
    id SERIAL PRIMARY KEY
);

CREATE TABLE order_items
(
    id       SERIAL PRIMARY KEY,
    order_id INTEGER NOT NULL REFERENCES orders (id)
);

and this config:

psql:
  driver: github.com/jackc/pgx/v5/stdlib
  type_system: database/sql
  output: test/db
  pkgname: dbtest
  schemas:
    - public
  uuid_pkg: gofrs

When I try to create an order with 2 order items, the factory inserts a new order for every child item, so 3 orders are created in total.

func TestChild(t *testing.T) {
	f := factory.New()

	// orders should be created with 2 items
	f.AddBaseOrderMod(
		factory.OrderMods.WithNewOrderItems(2),
	)
	// doesn't do anything
	f.AddBaseOrderItemMod(
		factory.OrderItemMods.WithoutOrder(),
	)

	db := must(sql.Open("postgres", "dbname=bob sslmode=disable"))

	bobDB := bob.NewDB(db)
	ctx := context.Background()
	withRollback(ctx, bobDB, func(tx bob.Executor) error {
		_, err := f.NewOrder().Create(ctx, tx)
		if err != nil {
			return err
		}
		total, err := models.Orders.Query().Count(ctx, tx)
		if err != nil {
			return err
		}

		t.Log(total) // prints 3, expected 1

		return nil
	})
}

func must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}

func withRollback(ctx context.Context, db bob.DB, fn func(tx bob.Executor) error) {
	tx := must(db.BeginTx(ctx, nil))
	defer tx.Rollback(ctx)

	exec := bob.Debug(tx)
	if err := fn(exec); err != nil {
		panic(err)
	}
}
test output
=== RUN   TestChild
INSERT INTO "orders" AS "orders"("id")
VALUES (DEFAULT)
RETURNING "orders"."id" AS "id"


INSERT INTO "orders" AS "orders"("id")
VALUES (DEFAULT)
RETURNING "orders"."id" AS "id"


INSERT INTO "order_items" AS "order_items"("id", "order_id")
VALUES (DEFAULT, $1)
RETURNING "order_items"."id" AS "id", "order_items"."order_id" AS "order_id"

0: int32: 17

INSERT INTO "orders" AS "orders"("id")
VALUES (DEFAULT)
RETURNING "orders"."id" AS "id"


INSERT INTO "order_items" AS "order_items"("id", "order_id")
VALUES (DEFAULT, $1)
RETURNING "order_items"."id" AS "id", "order_items"."order_id" AS "order_id"

0: int32: 18

UPDATE "order_items" AS "order_items" SET
"order_id" = $1
WHERE ("order_items"."id" IN ($2, $3))
RETURNING "order_items"."id" AS "id", "order_items"."order_id" AS "order_id"
0: omit.Val[int32]: 16
1: int32: 11
2: int32: 12

SELECT 
count(1)
FROM "orders" AS "orders"
LIMIT 1
OFFSET 0


    db_test.go:55: 3
--- PASS: TestChild (0.04s)

The issue seems to be caused by this bit.

if o.r.{{$relAlias}} == nil {
{{$tAlias.UpSingular}}Mods.WithNew{{$relAlias}}().Apply(ctx, o)

Adding .OrderItemMods.WithoutOrder() doesn't do anything, because the generated code looks like this, which always creates a parent even though we told it not to.

func (m orderItemMods) WithoutOrder() OrderItemMod {
	return OrderItemModFunc(func(ctx context.Context, o *OrderItemTemplate) {
		o.r.Order = nil
	})
}

func (o *OrderItemTemplate) Create(ctx context.Context, exec bob.Executor) (*models.OrderItem, error) {
	var err error
	opt := o.BuildSetter()
	ensureCreatableOrderItem(opt)

	if o.r.Order == nil {
		OrderItemMods.WithNewOrder().Apply(ctx, o)
	}

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions