Group by column and scan into custom struct - go

I have the following model. Each instance is part of a group which is only defined as the string GroupName here because the actual group is defined in a different service using a different database.
type Instance struct {
gorm.Model
UserID uint
Name string `gorm:"index:idx_name_and_group,unique"`
GroupName string `gorm:"index:idx_name_and_group,unique"`
StackName string
DeployLog string `gorm:"type:text"`
Preset bool
PresetID uint
}
I'd like to scan, the above model, into the following struct. Thus grouping instances why their group name.
type GroupWithInstances struct {
Name string
Instances []*model.Instance
}
I'm been trying my luck with the following gorm code
var result []GroupWithInstances
err := r.db.
Model(&model.Instance{}).
Where("group_name IN ?", names).
Where("preset = ?", presets).
Group("group_name").
Scan(&result).Error
indent, _ := json.MarshalIndent(result, "", " ")
log.Println(string(indent))
But I'm getting the following error
ERROR: column "instances.id" must appear in the GROUP BY clause or be used in an aggregate function (SQLSTATE 42803)
I'm not sure how to deal with that since I don't want to group by instances but rather their groups.

The error indicates that your RDBMS is in Full Group By Mode - cannot select field that isn't in group by clause or used in an aggregate function (SUM, AVG,...). There are 2 solutions:
Disable Full Group By mode. Example in MySQL
Modify the query
Even when we go with Solution 1, gorm will throw another error about relationship between GroupWithInstances and Instance.
So I think we should review the feature and go with Solution 1 - only select what is needed.

Related

many to many in gorm v2 error on foreign key

I'm finding it difficult to define many to many relationship using Gorm in following cases
features(feature_id, name, slug)
operations(operation_id, name, slug)
feature_operations(feature_id, operation_id)
type Feature struct {
FeatureID int64 `gorm:"primaryKey;column:feature_id" json:"feature_id"`
Name string `validate:"required" json:"name"`
Slug string `json:"slug"`
Status string `json:"status"`
Operations []Operation `gorm:"many2many:feature_operations;foreignKey:feature_id"`
appModels.BaseModel
}
When using feature_id, I get error
column feature_operations.feature_feature_id does not exist
When using id, I get error
invalid foreign key: id
Looks like you are not using the convention that gorm suggests where you name your primary key columns just id
so in your case your foreignKey should be the name of the field and you also need to use References to specify column that you want to reference. See the example here:
https://gorm.io/docs/many_to_many.html#Override-Foreign-Key
What you need is this:
type Feature struct {
FeatureID int64 `gorm:"primaryKey;column:feature_id"`
Name string
Slug string
Operations []Operation `gorm:"many2many:feature_operations;foreignKey:FeatureID;References:OperationID"`
}
type Operation struct {
OperationID int64 `gorm:"primaryKey;column:operation_id"`
Name string
Slug string
}
After this the join table will be FEATURE_OPERATIONS with two columns FEATURE_FEATURE_ID AND OPERATION_OPERATION_ID
If you dont like the redundant column names then you need to use the two additional attributes joinForeignKey and joinReferences to choose your own names for the columns like so:
gorm:"many2many:feature_operations;foreignKey:FeatureID;joinForeignKey:FeatureID;References:OperationID;joinReferences:OperationID"
All this extra work is needed because your primary keys are FEATURE_ID and OPERATION_ID instead of just ID
If you can rename the column to follow the convention, you will notice life is much easier with gorm

How to search on a many-to-many field

Let's assume I have the below structs
type Job struct {
ID string `sql:"type:uuid;primary_key;"`
Title string `json:"title,omitempty"`
Skills []*skill.Skill `json:"skills,omitempty"gorm:"many2many:job_skill;"`
}
type Skill struct {
Name string `json:"name" gorm:"primary_key"`
}
To query all the jobs I do:
jobs := &[]Job{}
gorm.DB.Preload("Skills").Find(&jobs)
How do I search for a Job that contains a certain skill? I have tried the below but it says the column does not exist.
s := "golang"
jobs := &[]Job{}
gorm.DB.Preload("Skills").Where("skill = ?", s).Find(&jobs)
I can see my issues, = doesn't seem to be the correct operator as it needs to search in a list. And it also isn't loading the join table as I assumed it would
Debug output
pq: column "skill" does not exist
SELECT * FROM "jobs" WHERE skill = 'golang'
The Preload method and the Associations feature help you load your fields by constructing basic SQL queries based on the relationships you have created. Queries like loading all skills for a specific job (or jobs). But it doesn't go any more complex than that.
Rather, think of go-gorm as a way to construct your SQL queries and load the data into your models.
Having that in mind, one solution would be to use Joins to include the skill table.
gorm.DB.Preload("Skills")
.Joins("INNER JOIN job_skill js ON js.job_id = jobs.id").
.Joins("INNER JOIN skill s ON s.id = js.skill_id").
.Where("s.name = ?", s).Find(&jobs)

"Creation At" time in GORM Customise Join table

I am trying to customize many2many table join. I have two tables from which I want to have taken the ids and want another field, which will tell me when the entry in the join table was made. The ids are coming fine, but the "created_at" is not updating and shows "Null" instead of time.
// this is the table join struct which I want to make
type UserChallenges struct {
gorm.JoinTableHandler
CreatedAt time.Time
UserID int
ChallengeID int
}
//hook before create
func (UserChallenges) BeforeCreate(Db \*gorm.DB) error {
Db.SetJoinTableHandler(&User{}, "ChallengeId", &UserChallenges{})
return nil
}
This is not giving any error on the build. Please tell me what I am missing so that I can get the creation time field in this.
PS - The documentation of GORM on gorm.io is still showing SetupJoinTable method but it is deprecated in the newer version. There is a SetJoinTableHandler but there is no documentation available for it anywhere.
The thing to get about using a Join Table model is that if you want to access fields inside the model, you must query it explicitly.
That is using db.Model(&User{ID: 1}).Association("Challenges").Find(&challenges) or db.Preload("Challenges").Find(&users), etc. will just give you collections of the associated struct and in those there is no place in which to put the extra fields!
For that you would do:
joins := []UserChallenges{}
db.Where("user_id = ?", user.ID).Find(&joins)
// now joins contains all the records in the join table pertaining to user.ID,
// you can access joins[i].CreatedAt for example.
If you wanted also to retrieve the Challenges with that, you could modify your join struct to integrate the BelongsTo relation that it has with Challenge and preload it:
type UserChallenges struct {
UserID int `gorm:"primaryKey"`
ChallengeID int `gorm:"primaryKey"`
Challenge Challenge
CreatedAt time.Time
}
joins := []UserChallenges{}
db.Where("user_id = ?", user.ID).Joins("Challenge").Find(&joins)
// now joins[i].Challenge is populated

Unable to update jsonb (ie subarray) column

I am just getting my feet wet with go-pg, and having some issues utilizing the JSONB capabilities of PostgreSQL DB.
I have defined my model structs as:
type Chemical struct {
Id int
ChemicalName string
ListingDetails []ListingDetail `sql:",array"`
ParentChemical *Chemical `pg:"fk_parent_chemical_id"`
ParentChemicalId int
}
type ListingDetail struct {
TypeOfToxicity string
ListingMechanism string
CasNo string
ListedDate time.Time
SafeHarborInfo string
}
In the Chemical struct - by using the tag sql:",array" on the ListingDetails field - my understanding is this should get mapped to a jsonb column in the destination table. Using go-pg's CreateTable - a chemicals table gets created with a listing_details jsonb column - so all is good in that regards.
However, I cant seem to update this field (in the db) successfully. When I initially create a chemical record in teh database - there are no ListingInfos - I go back up update them at a later time. Below is a snippet showing how I do that.
detail := models.ListingDetail{}
detail.TypeOfToxicity = strings.TrimSpace(row[1])
detail.ListingMechanism = strings.TrimSpace(row[2])
detail.CasNo = strings.TrimSpace(row[3])
detail.SafeHarborInfo = strings.TrimSpace(row[5])
chemical.ListingDetails = append(chemical.ListingDetails, detail)
err = configuration.Database.Update(&chemical)
if err != nil {
panic(err)
}
But I always get error message shown below. What am I doing wrong??
panic: ERROR #22P02 malformed array literal: "{{"TypeOfToxicity":"cancer","ListingMechanism":"AB","CasNo":"26148-68-5","ListedDate":"1990-01-01T00:00:00Z","SafeHarborInfo":"2"}}"

Sqlx "missing destination name" for struct tag through pointer

I have a model like this:
type Course struct {
Name string `db:"course_name"`
}
type Group struct {
Course *Course
}
type Groups []Group
And when i try to do sqlx.Select for Groups with a query like this:
SELECT c.name as course_name FROM courses as c;
I get
missing destination name course_name in *main.Groups
error.
What is the issue with this code?
You need to use sqlx.Select when you are selecting multiple rows and you want to scan the result into a slice, as is the case for your query, otherwise use sqlx.Get for a single row.
In addition, you can't scan directly into a Group struct because none of its fields are tagged (unlike the Course struct), and the Course field isn't embedded.
Try:
course := Course{}
courses := []Course{}
db.Get(&course, "SELECT name AS course_name FROM courses LIMIT 1")
db.Select(&courses, "SELECT name AS course_name FROM courses")
I changed Course *Course to Course Course - no effect.
When i made it embedded like this:
type Group struct {
Course
}
it worked.
This is also valid:
type Group struct {
*Course
}
Looks like sqlx doesn't understand anything except embedded fields.
Ok, so when you are using an embbeded struct with Sqlx, capitalization is important, at least in my case.
You need to refer to the embedded struct in your sql, like so:
select name as "course.name" from courses...
Notice that course is lower case and the naming involves a dot/period between table and column.
Then your Get or Select should work just fine.

Resources