Start learning free

LESSON 129 OF 173 · BACKEND: SPEED

Insert Many at Once

Practice

A supplier sends the shop a list of new products. One POST /products per product costs one request, one query and one commit each, and the commit is the slow part: it waits until the disk has really stored the data. A batch sends the whole list in one request, and the shop saves it with one commit.

One transaction for many rows: Go's database/sql has no executemany, so you run one statement once for each row of a list, all inside one transaction that db.Begin() opens and tx.Commit() saves. Each row is a []any, with its values in the order of the ? marks. Here it adds two lines to order 4 at once, 2 mugs and 1 lamp:

items := [][]any{{4, 1, 2}, {4, 3, 1}}
tx, _ := db.Begin()
for _, item := range items {
	tx.Exec("INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)", item...)
}
tx.Commit()

All or nothing: if one row is refused, like a product with no price in a NOT NULL column, tx.Exec returns an error when it reaches that row. The rows before it are inserted but not saved yet: they wait in the open transaction, and tx.Rollback() throws them all away. The client can fix its list and send it again without making doubles.

GET /products  →  200  [{"id": 1, "name": "Mug", "price": 12}, …, {"id": 8, "name": "Gift card", "price": 50}]
POST /products/batch  [{"name": "Cup", "price": 8}, {"name": "Bowl", "price": 11}, {"name": "Vase", "price": 18}]
    →  201  {"added": 3}
GET /debug/queries  →  200  {"queries": 2}   (1 for the first GET /products, 1 for the whole batch)
GET /products  →  200  [… the 8 products …, {"id": 9, "name": "Cup", "price": 8}, {"id": 10, "name": "Bowl", …}, {"id": 11, "name": "Vase", …}]
POST /products/batch  [{"name": "Tray", "price": 6}, {"name": "Clock"}]
    →  400  {"error": "Each product needs a name and a price"}
GET /products/12  →  404  {"error": "Product not found"}

Your task: the helper runMany(tx, query, rows) is given: it counts one query and loops over the rows; make it run query with each row's values in tx. Then finish POST /products/batch, which already turns the JSON list into []any{name, price} rows: begin a transaction, insert them all with one runMany call, roll back and answer 400 if a row is refused, and otherwise commit once and clear the cache, so the handler answers {"added": …} with 201.


Assignment
Follow the Coding Tutorial to finish runMany and the POST /products/batch handler.


Hint
A missing price is nil in item["price"], and the database refuses it for you. Put tx.Commit() below the if block: the return inside the if makes sure the commit runs only when every row went in.


STEP 1 OF 9
    Ctrl + Enter to check