Go SQL map 1:M relation json array to slice - postgresql

I have a product-image (1:M) relation and basically i want to map those images to a slice in my struct. I am using sqlx library to make it bit easier.
I searched for a while and maybe the best answer was in this thread: Efficiently mapping one-to-many many-to-many database to struct in Golang.
The answer with creating the view and returning everything as a json works but it feels somehow hacky.
Ideally what i want to do is use postgres json_agg to return the json array inside the images column that would match my type so i could scan into it.
I did this with nodejs several times but here i don't know how. Do i need to implement custom Scan and Value methods or is there a simpler way. I didn't show this but i also have a category that is 1:1 and i can embed that category in product and do a left join and it works with sqlx, but not with images type.
Simplified models
type Image struct {
ID int,
URL string,
ProductID int
}
type ImageList []*Image
type Product struct {
ID int `db:"id"`
Name string `db:"name"`
Images ImageList `db:"images"`
}
DB Tables
create table product (
id int generated always as identity primary key,
name varchar(255),
);
create table product_image (
id int generated always as identity primary key,
url text not null,
product_id int references product(id)
);
I am trying something like this now:
q := `SELECT
p.*,
COALESCE(JSON_AGG(img.*) FILTER (WHERE img.product_id IS NOT NULL), '[]'::json) AS images
FROM product p
LEFT JOIN product_image img ON p.id = img.product_id
WHERE p.id = 1
GROUP BY p.id`
var p Product
if err := sqlxdb.Get(&p, q); err != nil {
fmt.Printf("Error: %v\n", err)
}
I get this error:
sql: Scan error on column index 26, name "images": unsupported Scan, storing driver.Value type []uint8 into type *model.ImageList
This seems like a super common scenario and yet i can't find any examples...
Or finally am i even stupid for doing this because i can just do forEach in JS and do like 50 queries to fetch all the images for every product.

One solution for getting list of items could be done using this mapping lib: carta
q := `SELECT p.*, img.id AS img_id, img.url AS img_url
FROM public.product p
LEFT JOIN product_image img ON p.id = img.product_id`
rows, err := sqlxdb.Query(q)
if err != nil {
fmt.Println(err)
}
var products []*model.Product
carta.Map(rows, &products)
And for the img struct i would use db:"img_id" prefix and so on, because i select with alias...

Related

GORM unable to update data in one to many relationship

I have two tables users and documents. They are related in such a way that each document must belong to a user using a one to many relationship. When I try updating a document I get the following error
ERROR: insert or update on table "documents" violates foreign key
constraint "fk_users_documents" (SQLSTATE 23503)
Here are my structs definition and update function
type User struct {
gorm.Model
Name string
Email string
Password string
Documents []Document
}
type Document struct {
gorm.Model
Name string
UserID uint
}
//Update document by id
func (h handler)UpdateDocument(w http.ResponseWriter, r *http.Request) {
// once again, we will need to parse the path parameters
var updatedDoc Document
reqBody, _ := ioutil.ReadAll(r.Body)
json.Unmarshal(reqBody, &updatedDoc)
var document Document
vars := mux.Vars(r)
id := vars["id"]
if result := Db.First(&updatedDoc, id); result.Error != nil {
fmt.Println(result.Error)
}
document.Name=updatedDoc.Name
Db.Save(&document)
json.NewEncoder(w).Encode(&updatedDoc)
}
You are calling Db.Save(&document) but document has only its Name field populated. This means that the UserID is set to 0. I'm guessing you don't have any user with ID 0 present in the User table, therefore this violates the foreign key constraint.
The UserID field shall always be set to an existing user when updating a document otherwise the query will fail.
Regardless of this, I'd suggest you to study a bit of database and golang basics because the code you posted is quite messy.

Correct way to access data from postgresql using go-pgsql in GoLang

I've been reading the GoLang go-pgsql documentation in order to figure out how to access data with nested objects, but I have so far been unsuccessful.
Here's the description of what I am trying to achieve:
I have two models ClimateQuestions and Steps:
type ClimateQuestions struct {
tableName struct{} `pg:"climatequestions"`
Id int `json:"id" pg:",pk"`
Title string `json:"title"`
Steps []*Steps `pg:"rel:has-many"`
}
type Steps struct {
tableName struct{} `pg:"steps"`
Id int `json:"id"`
Label string `json:"label"`
Number int `json:"number"`
QuestionId int `json:"question_id"`
}
and here is how they're defined in the database:
CREATE TABLE climatequestions (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);
CREATE TABLE steps (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
value DOUBLE PRECISION NOT NULL,
question_id INT REFERENCES climatequestions(id)
);
And a relationship between these models is this: For every climate question, there can be many steps. I have denoted this by adding a field in ClimateQuestions struct called Steps with rel:has-many.
Now, from the database, I would like to obtain all the climate questions, and within each of them, I want an array of steps data.
My first method to achieve this has been the following:
var climateQuestions []model.ClimateQuestions
err := db.Model(&climateQuestions).Select()
This partially works in that it returns all the data relevant for climate questions, but it does not add the nested steps data. Here is the JSON format of what is returned:
[{"id":1,"title":"first question?","Steps":null},{"id":2,"title":"second question?","Steps":null}]
Any ideas as to how I may achieve this?
Because you have custom join foreign key, you need to add tag pg:"rel:has-many,join_fk:question_id" in ClimateQuestions.Steps
In struct Steps, you need to tell pg which field it is.
You forgot to call Relation function
this is the correct struct
type ClimateQuestions struct {
tableName struct{} `pg:"climatequestions"`
Id int `json:"id" pg:",pk"`
Title string `json:"title"`
Steps []*Steps `pg:"rel:has-many,join_fk:question_id"`
}
type Steps struct {
tableName struct{} `pg:"steps"`
Id int `json:"id"`
Label string `json:"label" pg:"title"`
Number int `json:"number" pg:"value"`
QuestionId int `json:"question_id"`
}
this is how you should exec db.
var climateQuestions []ClimateQuestions
err := db.Model(&climateQuestions).Relation("Steps").Select()
if err != nil {
panic(err.Error())
}
for _, v := range climateQuestions {
fmt.Printf("%#v\n", v)
for _, v1 := range v.Steps {
fmt.Printf("%#v\n", v1)
}
fmt.Println("")
}

Get association objects with order from JoinTable using GORM

I have two models
type Holder struct {
OwnModel
Title string `json:"title"`
Exercises []Exercise `json:"exercises" gorm:"many2many:holder_exercises_new;"`
}
type Exercise struct {
OwnModel
Title string `json:"title"`
}
And join table
type HolderExercisesNew struct {
OwnModel
HolderID int64 `json:"holder_id"`
ExerciseID int64 `json:"exercise_id"`
Order int64 `json:"order"` // my custom param for ordering
}
I want to preload Exercises to Holders but ordered by "Order" param in join table.
For example i have
holders := []models.Holder{}
database.Preload("Exercises").Find(&holders)
It gives me holders with embedded exercises but in random order.
Is there a way to get Exercises in order set in join table?
I tried what seemed obvious to me, which is using Custom Preloading SQL:
holders := []models.Holder{}
database.
Preload("Exercises", func(tx *gorm.DB) *gorm.DB {
return tx.Order("holder_exercises_new.order ASC")
}).
Find(&holders)
But that didn't work because GORM separates the loading into two queries, one of the join table and another of the joined table. A workaround I found would be, sans error checking:
holders := []models.Holder{}
database.Find(&holders)
for i := range holders {
database.
Model(&holders[i]).
Order("holder_exercises_new.order ASC").
Association("Exercises").
Find(&holders[i].Exercises)
}

Golang record ids are 0

I have the following Car model and a cars slice:
type Car struct {
ID int `json:"id"`
Name string `json:"title"`
}
var cars []Car
On a Postgres database, I have created a cars table to save car records.
create table cars (id serial, name varchar);
And saved, a couple of car records:
insert into cars (name) values ('Toyota');
insert into cars (name) values ('Lexus');
The records are created successfully with incremental integer ids 1 and 2.
In my Go server, I am making the following query to get the car records:
db.Query("SELECT * from cars").Rows(&cars)
for _, car := range cars {
fmt.Println(car)
}
Although there is a response, the id for each record comes as 0. I have tried to find out why, but I was not able to. Anyone has any idea?
database/sql package does not provide to scan the data directly to a struct, you should iterate over the rows returned from the database query and then scan the data into struct as:
var cars []cars
for rows.Next() {
var car cars
err = rows.Scan(&c.ID, &c.Name)
if err != nil {
log.Fatalf("Scan: %v", err)
}
cars = append(cars, car)
}
fmt.Println(cars)
Or you can use extension for sql package name sqlx which will provide you to directly scan the result into the slice of struct as:
cars := []cars{}
db.Select(&cars, "SELECT * from cars")

How to dynamically set table name for every query in go-pg?

I have a bunch of similar temp tables which I am trying to query using go-pg's ORM. I can't find a way to dynamically change the queried table during a select:
import "gopkg.in/pg.v4"
type MyModel struct {
TableName struct{} `sql:"temp_table1"`
Id int64
Name string
}
var mymodels []MyModel
err := db.Model(&mymodels).Column("mymodel.id", "mymodel.name").Select()
This will query temp_table1 as defined in the model's TableName. Is there a way to pass table name as a parameter so I can query temp_table_X?
(I can just not use ORM and go with raw db.Query(), but I wanted to see if there is a way to use ORM).
Got an answer on github:
err := db.Model().TableExpr("temp_table_999 AS mymodel").Column("mymodel.id", "mymodel.name").Select(&mymodels)
Seems you can specify the table directly: db.Model(&mymodels).Table('temp_table1').Column("mymodel.id", "mymodel.name").Select()