autoworkflow 3.1.5 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# GORM Skill
|
|
2
|
+
|
|
3
|
+
## Database Connection
|
|
4
|
+
\`\`\`go
|
|
5
|
+
package database
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"fmt"
|
|
9
|
+
"log"
|
|
10
|
+
"os"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"gorm.io/driver/postgres"
|
|
14
|
+
"gorm.io/gorm"
|
|
15
|
+
"gorm.io/gorm/logger"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func Connect() (*gorm.DB, error) {
|
|
19
|
+
dsn := os.Getenv("DATABASE_URL")
|
|
20
|
+
|
|
21
|
+
// Configure logger
|
|
22
|
+
gormLogger := logger.New(
|
|
23
|
+
log.New(os.Stdout, "\\r\\n", log.LstdFlags),
|
|
24
|
+
logger.Config{
|
|
25
|
+
SlowThreshold: 200 * time.Millisecond,
|
|
26
|
+
LogLevel: logger.Warn,
|
|
27
|
+
IgnoreRecordNotFoundError: true,
|
|
28
|
+
Colorful: true,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
33
|
+
Logger: gormLogger,
|
|
34
|
+
SkipDefaultTransaction: true, // Disable for performance
|
|
35
|
+
PrepareStmt: true, // Cache prepared statements
|
|
36
|
+
})
|
|
37
|
+
if err != nil {
|
|
38
|
+
return nil, fmt.Errorf("connect to database: %w", err)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Configure connection pool
|
|
42
|
+
sqlDB, err := db.DB()
|
|
43
|
+
if err != nil {
|
|
44
|
+
return nil, err
|
|
45
|
+
}
|
|
46
|
+
sqlDB.SetMaxIdleConns(10)
|
|
47
|
+
sqlDB.SetMaxOpenConns(100)
|
|
48
|
+
sqlDB.SetConnMaxLifetime(time.Hour)
|
|
49
|
+
|
|
50
|
+
return db, nil
|
|
51
|
+
}
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
## Model Definition with Associations
|
|
55
|
+
\`\`\`go
|
|
56
|
+
package model
|
|
57
|
+
|
|
58
|
+
import (
|
|
59
|
+
"time"
|
|
60
|
+
|
|
61
|
+
"gorm.io/gorm"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Base model (alternative to gorm.Model with UUID)
|
|
65
|
+
type Base struct {
|
|
66
|
+
ID string \`gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"\`
|
|
67
|
+
CreatedAt time.Time \`json:"created_at"\`
|
|
68
|
+
UpdatedAt time.Time \`json:"updated_at"\`
|
|
69
|
+
DeletedAt gorm.DeletedAt \`gorm:"index" json:"-"\`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type User struct {
|
|
73
|
+
Base
|
|
74
|
+
Email string \`gorm:"uniqueIndex;size:255;not null" json:"email"\`
|
|
75
|
+
Name string \`gorm:"size:100;not null" json:"name"\`
|
|
76
|
+
Password string \`gorm:"size:255;not null" json:"-"\`
|
|
77
|
+
IsActive bool \`gorm:"default:true" json:"is_active"\`
|
|
78
|
+
|
|
79
|
+
// Associations
|
|
80
|
+
Profile *Profile \`gorm:"constraint:OnDelete:CASCADE" json:"profile,omitempty"\`
|
|
81
|
+
Posts []Post \`gorm:"constraint:OnDelete:CASCADE" json:"posts,omitempty"\`
|
|
82
|
+
Roles []Role \`gorm:"many2many:user_roles" json:"roles,omitempty"\`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Profile struct {
|
|
86
|
+
ID string \`gorm:"type:uuid;primaryKey;default:gen_random_uuid()"\`
|
|
87
|
+
UserID string \`gorm:"type:uuid;uniqueIndex"\`
|
|
88
|
+
Bio string \`gorm:"type:text"\`
|
|
89
|
+
AvatarURL string \`gorm:"size:500"\`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type Post struct {
|
|
93
|
+
Base
|
|
94
|
+
Title string \`gorm:"size:255;not null"\`
|
|
95
|
+
Content string \`gorm:"type:text"\`
|
|
96
|
+
Published bool \`gorm:"default:false"\`
|
|
97
|
+
AuthorID string \`gorm:"type:uuid;index"\`
|
|
98
|
+
|
|
99
|
+
Author *User \`gorm:"foreignKey:AuthorID" json:"author,omitempty"\`
|
|
100
|
+
Tags []Tag \`gorm:"many2many:post_tags" json:"tags,omitempty"\`
|
|
101
|
+
Comments []Comment \`gorm:"constraint:OnDelete:CASCADE" json:"comments,omitempty"\`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Tag struct {
|
|
105
|
+
ID uint \`gorm:"primaryKey"\`
|
|
106
|
+
Name string \`gorm:"uniqueIndex;size:50"\`
|
|
107
|
+
Posts []Post \`gorm:"many2many:post_tags"\`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type Comment struct {
|
|
111
|
+
Base
|
|
112
|
+
PostID string \`gorm:"type:uuid;index"\`
|
|
113
|
+
UserID string \`gorm:"type:uuid;index"\`
|
|
114
|
+
Content string \`gorm:"type:text;not null"\`
|
|
115
|
+
|
|
116
|
+
User *User \`gorm:"foreignKey:UserID"\`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type Role struct {
|
|
120
|
+
ID uint \`gorm:"primaryKey"\`
|
|
121
|
+
Name string \`gorm:"uniqueIndex;size:50"\`
|
|
122
|
+
Users []User \`gorm:"many2many:user_roles"\`
|
|
123
|
+
}
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
## Hooks (Callbacks)
|
|
127
|
+
\`\`\`go
|
|
128
|
+
import (
|
|
129
|
+
"golang.org/x/crypto/bcrypt"
|
|
130
|
+
"gorm.io/gorm"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// BeforeCreate hook - hash password
|
|
134
|
+
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
|
135
|
+
if u.Password != "" {
|
|
136
|
+
hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
|
137
|
+
if err != nil {
|
|
138
|
+
return err
|
|
139
|
+
}
|
|
140
|
+
u.Password = string(hashed)
|
|
141
|
+
}
|
|
142
|
+
return nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// AfterCreate hook - send welcome email
|
|
146
|
+
func (u *User) AfterCreate(tx *gorm.DB) error {
|
|
147
|
+
// Queue welcome email (don't block)
|
|
148
|
+
go sendWelcomeEmail(u.Email)
|
|
149
|
+
return nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// BeforeUpdate hook - validate changes
|
|
153
|
+
func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
|
154
|
+
// Hash password if changed
|
|
155
|
+
if tx.Statement.Changed("Password") {
|
|
156
|
+
hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
|
157
|
+
if err != nil {
|
|
158
|
+
return err
|
|
159
|
+
}
|
|
160
|
+
tx.Statement.SetColumn("Password", string(hashed))
|
|
161
|
+
}
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// BeforeDelete hook - cleanup related data
|
|
166
|
+
func (u *User) BeforeDelete(tx *gorm.DB) error {
|
|
167
|
+
// Archive user data before soft delete
|
|
168
|
+
return tx.Model(&Post{}).Where("author_id = ?", u.ID).Update("author_id", nil).Error
|
|
169
|
+
}
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
## CRUD Operations
|
|
173
|
+
\`\`\`go
|
|
174
|
+
package repository
|
|
175
|
+
|
|
176
|
+
import (
|
|
177
|
+
"context"
|
|
178
|
+
"errors"
|
|
179
|
+
|
|
180
|
+
"gorm.io/gorm"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
var ErrNotFound = errors.New("record not found")
|
|
184
|
+
|
|
185
|
+
type UserRepository struct {
|
|
186
|
+
db *gorm.DB
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func NewUserRepository(db *gorm.DB) *UserRepository {
|
|
190
|
+
return &UserRepository{db: db}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create
|
|
194
|
+
func (r *UserRepository) Create(ctx context.Context, user *model.User) error {
|
|
195
|
+
return r.db.WithContext(ctx).Create(user).Error
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Find by ID with eager loading
|
|
199
|
+
func (r *UserRepository) GetByID(ctx context.Context, id string) (*model.User, error) {
|
|
200
|
+
var user model.User
|
|
201
|
+
err := r.db.WithContext(ctx).
|
|
202
|
+
Preload("Profile").
|
|
203
|
+
Preload("Roles").
|
|
204
|
+
First(&user, "id = ?", id).Error
|
|
205
|
+
|
|
206
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
207
|
+
return nil, ErrNotFound
|
|
208
|
+
}
|
|
209
|
+
return &user, err
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Find by email
|
|
213
|
+
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
|
|
214
|
+
var user model.User
|
|
215
|
+
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
|
|
216
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
217
|
+
return nil, ErrNotFound
|
|
218
|
+
}
|
|
219
|
+
return &user, err
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Update specific fields
|
|
223
|
+
func (r *UserRepository) Update(ctx context.Context, user *model.User) error {
|
|
224
|
+
return r.db.WithContext(ctx).
|
|
225
|
+
Model(user).
|
|
226
|
+
Select("Name", "Email", "IsActive"). // Only update these fields
|
|
227
|
+
Updates(user).Error
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Delete (soft delete with gorm.DeletedAt)
|
|
231
|
+
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
|
232
|
+
result := r.db.WithContext(ctx).Delete(&model.User{}, "id = ?", id)
|
|
233
|
+
if result.RowsAffected == 0 {
|
|
234
|
+
return ErrNotFound
|
|
235
|
+
}
|
|
236
|
+
return result.Error
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Hard delete (permanent)
|
|
240
|
+
func (r *UserRepository) HardDelete(ctx context.Context, id string) error {
|
|
241
|
+
return r.db.WithContext(ctx).Unscoped().Delete(&model.User{}, "id = ?", id).Error
|
|
242
|
+
}
|
|
243
|
+
\`\`\`
|
|
244
|
+
|
|
245
|
+
## Transactions
|
|
246
|
+
\`\`\`go
|
|
247
|
+
// Manual transaction
|
|
248
|
+
func (r *UserRepository) CreateWithProfile(ctx context.Context, user *model.User, profile *model.Profile) error {
|
|
249
|
+
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
250
|
+
if err := tx.Create(user).Error; err != nil {
|
|
251
|
+
return err // Rollback
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
profile.UserID = user.ID
|
|
255
|
+
if err := tx.Create(profile).Error; err != nil {
|
|
256
|
+
return err // Rollback
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return nil // Commit
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Nested transaction with savepoints
|
|
264
|
+
func (r *UserRepository) ComplexOperation(ctx context.Context) error {
|
|
265
|
+
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
266
|
+
// First operation
|
|
267
|
+
if err := tx.Create(&user1).Error; err != nil {
|
|
268
|
+
return err
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Nested transaction (savepoint)
|
|
272
|
+
err := tx.Transaction(func(tx2 *gorm.DB) error {
|
|
273
|
+
if err := tx2.Create(&user2).Error; err != nil {
|
|
274
|
+
return err // Rollback to savepoint
|
|
275
|
+
}
|
|
276
|
+
return nil
|
|
277
|
+
})
|
|
278
|
+
if err != nil {
|
|
279
|
+
// Savepoint rolled back, but outer transaction continues
|
|
280
|
+
log.Printf("nested transaction failed: %v", err)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return nil // Commit outer transaction
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
\`\`\`
|
|
287
|
+
|
|
288
|
+
## Scopes (Reusable Query Logic)
|
|
289
|
+
\`\`\`go
|
|
290
|
+
// Scope for active users only
|
|
291
|
+
func ActiveUsers(db *gorm.DB) *gorm.DB {
|
|
292
|
+
return db.Where("is_active = ?", true)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Scope for pagination
|
|
296
|
+
func Paginate(page, perPage int) func(db *gorm.DB) *gorm.DB {
|
|
297
|
+
return func(db *gorm.DB) *gorm.DB {
|
|
298
|
+
if page <= 0 {
|
|
299
|
+
page = 1
|
|
300
|
+
}
|
|
301
|
+
if perPage <= 0 || perPage > 100 {
|
|
302
|
+
perPage = 20
|
|
303
|
+
}
|
|
304
|
+
offset := (page - 1) * perPage
|
|
305
|
+
return db.Offset(offset).Limit(perPage)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Scope for date range
|
|
310
|
+
func CreatedBetween(start, end time.Time) func(db *gorm.DB) *gorm.DB {
|
|
311
|
+
return func(db *gorm.DB) *gorm.DB {
|
|
312
|
+
return db.Where("created_at BETWEEN ? AND ?", start, end)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Using scopes
|
|
317
|
+
func (r *UserRepository) ListActive(ctx context.Context, page, perPage int) ([]model.User, int64, error) {
|
|
318
|
+
var users []model.User
|
|
319
|
+
var total int64
|
|
320
|
+
|
|
321
|
+
db := r.db.WithContext(ctx).Model(&model.User{}).Scopes(ActiveUsers)
|
|
322
|
+
|
|
323
|
+
// Get total count
|
|
324
|
+
if err := db.Count(&total).Error; err != nil {
|
|
325
|
+
return nil, 0, err
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Get paginated results
|
|
329
|
+
err := db.Scopes(Paginate(page, perPage)).
|
|
330
|
+
Order("created_at DESC").
|
|
331
|
+
Find(&users).Error
|
|
332
|
+
|
|
333
|
+
return users, total, err
|
|
334
|
+
}
|
|
335
|
+
\`\`\`
|
|
336
|
+
|
|
337
|
+
## Complex Queries
|
|
338
|
+
\`\`\`go
|
|
339
|
+
// Join with subquery
|
|
340
|
+
func (r *PostRepository) GetPopularPosts(ctx context.Context, minComments int) ([]model.Post, error) {
|
|
341
|
+
var posts []model.Post
|
|
342
|
+
|
|
343
|
+
subquery := r.db.Model(&model.Comment{}).
|
|
344
|
+
Select("post_id, COUNT(*) as comment_count").
|
|
345
|
+
Group("post_id").
|
|
346
|
+
Having("COUNT(*) >= ?", minComments)
|
|
347
|
+
|
|
348
|
+
err := r.db.WithContext(ctx).
|
|
349
|
+
Joins("JOIN (?) AS pc ON posts.id = pc.post_id", subquery).
|
|
350
|
+
Preload("Author").
|
|
351
|
+
Preload("Tags").
|
|
352
|
+
Order("pc.comment_count DESC").
|
|
353
|
+
Find(&posts).Error
|
|
354
|
+
|
|
355
|
+
return posts, err
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Raw SQL when needed
|
|
359
|
+
func (r *UserRepository) GetUserStats(ctx context.Context, userID string) (*UserStats, error) {
|
|
360
|
+
var stats UserStats
|
|
361
|
+
err := r.db.WithContext(ctx).Raw(\`
|
|
362
|
+
SELECT
|
|
363
|
+
u.id,
|
|
364
|
+
u.name,
|
|
365
|
+
COUNT(DISTINCT p.id) as post_count,
|
|
366
|
+
COUNT(DISTINCT c.id) as comment_count
|
|
367
|
+
FROM users u
|
|
368
|
+
LEFT JOIN posts p ON p.author_id = u.id
|
|
369
|
+
LEFT JOIN comments c ON c.user_id = u.id
|
|
370
|
+
WHERE u.id = ?
|
|
371
|
+
GROUP BY u.id, u.name
|
|
372
|
+
\`, userID).Scan(&stats).Error
|
|
373
|
+
|
|
374
|
+
return &stats, err
|
|
375
|
+
}
|
|
376
|
+
\`\`\`
|
|
377
|
+
|
|
378
|
+
## Migrations
|
|
379
|
+
\`\`\`go
|
|
380
|
+
func AutoMigrate(db *gorm.DB) error {
|
|
381
|
+
return db.AutoMigrate(
|
|
382
|
+
&model.User{},
|
|
383
|
+
&model.Profile{},
|
|
384
|
+
&model.Post{},
|
|
385
|
+
&model.Tag{},
|
|
386
|
+
&model.Comment{},
|
|
387
|
+
&model.Role{},
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// For production, use a migration tool like golang-migrate
|
|
392
|
+
\`\`\`
|
|
393
|
+
|
|
394
|
+
## ✅ DO
|
|
395
|
+
- Use \`WithContext(ctx)\` for all database operations
|
|
396
|
+
- Use \`Preload\` for eager loading associations
|
|
397
|
+
- Use scopes for reusable query logic
|
|
398
|
+
- Use transactions for multi-step operations
|
|
399
|
+
- Use \`Select\` to limit fields updated
|
|
400
|
+
- Handle \`gorm.ErrRecordNotFound\` explicitly
|
|
401
|
+
- Use prepared statements (\`PrepareStmt: true\`)
|
|
402
|
+
|
|
403
|
+
## ❌ DON'T
|
|
404
|
+
- Don't use \`Save()\` for partial updates (updates all fields)
|
|
405
|
+
- Don't ignore hook errors
|
|
406
|
+
- Don't use \`Find\` for single records (use \`First\` or \`Take\`)
|
|
407
|
+
- Don't forget \`Unscoped()\` when querying soft-deleted records
|
|
408
|
+
- Don't use \`AutoMigrate\` in production without review
|
|
409
|
+
- Don't use raw SQL without parameterized queries
|