Repository navigation
|
Hi. I'm trying to retrieve a user with a valid token using the Generics API. I want to join the tokens table to filter the user, but I get a panic whenever I execute this query. gorm v1.31.1 Models type Token struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"-"`
UserID uint `json:"-"`
Token string `gorm:"unique;type:varchar(64);not null" header:"x-token" json:"token" binding:"required"`
Expire time.Time `json:"expire"`
}
type User struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement"`
FirstName string `gorm:"type:varchar(32);not null"`
LastName string `gorm:"type:varchar(32);not null"`
Email string `gorm:"uniqueIndex;type:varchar(128);not null"`
PasswordHash string `gorm:"type:varchar;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt sql.NullTime
Tokens []Token `gorm:"foreignKey:UserID;references:ID"`
Accounts []Account `gorm:"foreignKey:UserID;references:ID"`
}FindUserByToken func (d *GormDatabase) FindUserByToken(ctx context.Context, token *model.Token) (*model.User, error) {
user, err := gorm.G[model.User](d.db).Joins(
clause.Has("Tokens"), func(db gorm.JoinBuilder, joinTable, curTable clause.Table) error {
db.Where("token = ? AND expire > ?", token.Token, time.Now())
return nil
}).Where("users.deleted_at is null").First(ctx)
if errors.Is(err, gorm.ErrRecordNotFound) {
err := errors.New("Invalid token")
err = model.NewApiError(err, 403, "auth")
return nil, err
}
return &user, err
}Error It seems like a type error. If I use Traditional API, everething works fine. func (d *GormDatabase) FindUserByToken(ctx context.Context, token *model.Token) (*model.User, error) {
var user model.User
err := d.db.WithContext(ctx).
Table("users").
Joins("INNER JOIN tokens ON tokens.user_id = users.id").
Where("tokens.token = ? AND tokens.expire > ?", token.Token, time.Now()).
Where("users.deleted_at IS NULL").
First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
err := errors.New("Invalid token")
err = model.NewApiError(err, 403, "auth")
return nil, err
} else if err != nil {
return nil, err
}
return &user, err
} |
Replies: 1 comment 2 replies
|
hey so i looked into this and its actually a bug in how the generics api handles HasMany joins during scanning. basically whats happening is:
the traditional api doesnt have this problem because when u pass a raw join string like if u check the test file workaround 1 - subquery (what i would do) func (d *GormDatabase) FindUserByToken(ctx context.Context, token *model.Token) (*model.User, error) {
user, err := gorm.G[model.User](d.db).
Where("id IN (?)", d.db.Model(&model.Token{}).
Select("user_id").
Where("token = ? AND expire > ?", token.Token, time.Now())).
Where("deleted_at IS NULL").
First(ctx)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, model.NewApiError(errors.New("Invalid token"), 403, "auth")
}
return &user, err
}this avoids the join completely and just filters with a subquery. still uses the generics api, no panic. also for this kind of query a subquery is arguably better than a join anyways since u dont get duplicate rows if a user has multiple matching tokens (shouldnt happen with unique constraint but still) workaround 2 - just use the traditional api for this one your working example is totally fine honestly. theres nothing wrong with mixing generics and traditional api in the same codebase. use generics where it works, fall back to traditional for HasMany joins until this gets fixed. workaround 3 - preload instead of joins user, err := gorm.G[model.User](d.db).
Preload("Tokens", func(pb gorm.PreloadBuilder) error {
pb.Where("token = ? AND expire > ?", token.Token, time.Now())
return nil
}).
Where("deleted_at IS NULL").
First(ctx)but this wont actually filter the User by the token, itll just load a User with their matching tokens preloaded. so you would need to check if id go with workaround 1 tbh. also u might wana open an issue for this, the fix should be in |
hey so i looked into this and its actually a bug in how the generics api handles HasMany joins during scanning.
basically whats happening is:
clause.Has("Tokens")makes gorm add column aliases likeTokens__id,Tokens__tokenetc to the SELECTscan.go, when it sees those aliased columns, it resolvesrel.Fieldfor theTokensfield on your User structscanIntoStruct, it callsReflectValueOfon that field to try and set the scanned valuesTokensis[]Token(a slice), andReflectValueOfatschema/field.go:511doesv.Field(fieldIndex)which only works on structs, not slices. so it panicsthe traditional api doesnt have this problem because when u pass a raw join string like
"INN…