1package tests_test 2 3import ( 4 "reflect" 5 "sort" 6 "testing" 7 8 "gorm.io/gorm" 9 . "gorm.io/gorm/utils/tests" 10) 11 12type Blog struct { 13 ID uint `gorm:"primary_key"` 14 Locale string `gorm:"primary_key"` 15 Subject string 16 Body string 17 Tags []Tag `gorm:"many2many:blog_tags;"` 18 SharedTags []Tag `gorm:"many2many:shared_blog_tags;ForeignKey:id;References:id"` 19 LocaleTags []Tag `gorm:"many2many:locale_blog_tags;ForeignKey:id,locale;References:id"` 20} 21 22type Tag struct { 23 ID uint `gorm:"primary_key"` 24 Locale string `gorm:"primary_key"` 25 Value string 26 Blogs []*Blog `gorm:"many2many:blog_tags"` 27} 28 29func compareTags(tags []Tag, contents []string) bool { 30 var tagContents []string 31 for _, tag := range tags { 32 tagContents = append(tagContents, tag.Value) 33 } 34 sort.Strings(tagContents) 35 sort.Strings(contents) 36 return reflect.DeepEqual(tagContents, contents) 37} 38 39func TestManyToManyWithMultiPrimaryKeys(t *testing.T) { 40 if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" { 41 t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment") 42 } 43 44 if name := DB.Dialector.Name(); name == "postgres" { 45 stmt := gorm.Statement{DB: DB} 46 stmt.Parse(&Blog{}) 47 stmt.Schema.LookUpField("ID").Unique = true 48 stmt.Parse(&Tag{}) 49 stmt.Schema.LookUpField("ID").Unique = true 50 // postgers only allow unique constraint matching given keys 51 } 52 53 DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags") 54 if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil { 55 t.Fatalf("Failed to auto migrate, got error: %v", err) 56 } 57 58 blog := Blog{ 59 Locale: "ZH", 60 Subject: "subject", 61 Body: "body", 62 Tags: []Tag{ 63 {Locale: "ZH", Value: "tag1"}, 64 {Locale: "ZH", Value: "tag2"}, 65 }, 66 } 67 68 DB.Save(&blog) 69 if !compareTags(blog.Tags, []string{"tag1", "tag2"}) { 70 t.Fatalf("Blog should has two tags") 71 } 72 73 // Append 74 var tag3 = &Tag{Locale: "ZH", Value: "tag3"} 75 DB.Model(&blog).Association("Tags").Append([]*Tag{tag3}) 76 77 if !compareTags(blog.Tags, []string{"tag1", "tag2", "tag3"}) { 78 t.Fatalf("Blog should has three tags after Append") 79 } 80 81 if count := DB.Model(&blog).Association("Tags").Count(); count != 3 { 82 t.Fatalf("Blog should has 3 tags after Append, got %v", count) 83 } 84 85 var tags []Tag 86 DB.Model(&blog).Association("Tags").Find(&tags) 87 if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) { 88 t.Fatalf("Should find 3 tags") 89 } 90 91 var blog1 Blog 92 DB.Preload("Tags").Find(&blog1) 93 if !compareTags(blog1.Tags, []string{"tag1", "tag2", "tag3"}) { 94 t.Fatalf("Preload many2many relations") 95 } 96 97 // Replace 98 var tag5 = &Tag{Locale: "ZH", Value: "tag5"} 99 var tag6 = &Tag{Locale: "ZH", Value: "tag6"} 100 DB.Model(&blog).Association("Tags").Replace(tag5, tag6) 101 var tags2 []Tag 102 DB.Model(&blog).Association("Tags").Find(&tags2) 103 if !compareTags(tags2, []string{"tag5", "tag6"}) { 104 t.Fatalf("Should find 2 tags after Replace") 105 } 106 107 if DB.Model(&blog).Association("Tags").Count() != 2 { 108 t.Fatalf("Blog should has three tags after Replace") 109 } 110 111 // Delete 112 DB.Model(&blog).Association("Tags").Delete(tag5) 113 var tags3 []Tag 114 DB.Model(&blog).Association("Tags").Find(&tags3) 115 if !compareTags(tags3, []string{"tag6"}) { 116 t.Fatalf("Should find 1 tags after Delete") 117 } 118 119 if DB.Model(&blog).Association("Tags").Count() != 1 { 120 t.Fatalf("Blog should has three tags after Delete") 121 } 122 123 DB.Model(&blog).Association("Tags").Delete(tag3) 124 var tags4 []Tag 125 DB.Model(&blog).Association("Tags").Find(&tags4) 126 if !compareTags(tags4, []string{"tag6"}) { 127 t.Fatalf("Tag should not be deleted when Delete with a unrelated tag") 128 } 129 130 // Clear 131 DB.Model(&blog).Association("Tags").Clear() 132 if DB.Model(&blog).Association("Tags").Count() != 0 { 133 t.Fatalf("All tags should be cleared") 134 } 135} 136 137func TestManyToManyWithCustomizedForeignKeys(t *testing.T) { 138 if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" { 139 t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment") 140 } 141 142 if name := DB.Dialector.Name(); name == "postgres" { 143 t.Skip("skip postgres due to it only allow unique constraint matching given keys") 144 } 145 146 DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags") 147 if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil { 148 t.Fatalf("Failed to auto migrate, got error: %v", err) 149 } 150 151 blog := Blog{ 152 Locale: "ZH", 153 Subject: "subject", 154 Body: "body", 155 SharedTags: []Tag{ 156 {Locale: "ZH", Value: "tag1"}, 157 {Locale: "ZH", Value: "tag2"}, 158 }, 159 } 160 DB.Save(&blog) 161 162 blog2 := Blog{ 163 ID: blog.ID, 164 Locale: "EN", 165 } 166 DB.Create(&blog2) 167 168 if !compareTags(blog.SharedTags, []string{"tag1", "tag2"}) { 169 t.Fatalf("Blog should has two tags") 170 } 171 172 // Append 173 var tag3 = &Tag{Locale: "ZH", Value: "tag3"} 174 DB.Model(&blog).Association("SharedTags").Append([]*Tag{tag3}) 175 if !compareTags(blog.SharedTags, []string{"tag1", "tag2", "tag3"}) { 176 t.Fatalf("Blog should has three tags after Append") 177 } 178 179 if DB.Model(&blog).Association("SharedTags").Count() != 3 { 180 t.Fatalf("Blog should has three tags after Append") 181 } 182 183 if DB.Model(&blog2).Association("SharedTags").Count() != 3 { 184 t.Fatalf("Blog should has three tags after Append") 185 } 186 187 var tags []Tag 188 DB.Model(&blog).Association("SharedTags").Find(&tags) 189 if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) { 190 t.Fatalf("Should find 3 tags") 191 } 192 193 DB.Model(&blog2).Association("SharedTags").Find(&tags) 194 if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) { 195 t.Fatalf("Should find 3 tags") 196 } 197 198 var blog1 Blog 199 DB.Preload("SharedTags").Find(&blog1) 200 if !compareTags(blog1.SharedTags, []string{"tag1", "tag2", "tag3"}) { 201 t.Fatalf("Preload many2many relations") 202 } 203 204 var tag4 = &Tag{Locale: "ZH", Value: "tag4"} 205 DB.Model(&blog2).Association("SharedTags").Append(tag4) 206 207 DB.Model(&blog).Association("SharedTags").Find(&tags) 208 if !compareTags(tags, []string{"tag1", "tag2", "tag3", "tag4"}) { 209 t.Fatalf("Should find 3 tags") 210 } 211 212 DB.Model(&blog2).Association("SharedTags").Find(&tags) 213 if !compareTags(tags, []string{"tag1", "tag2", "tag3", "tag4"}) { 214 t.Fatalf("Should find 3 tags") 215 } 216 217 // Replace 218 var tag5 = &Tag{Locale: "ZH", Value: "tag5"} 219 var tag6 = &Tag{Locale: "ZH", Value: "tag6"} 220 DB.Model(&blog2).Association("SharedTags").Replace(tag5, tag6) 221 var tags2 []Tag 222 DB.Model(&blog).Association("SharedTags").Find(&tags2) 223 if !compareTags(tags2, []string{"tag5", "tag6"}) { 224 t.Fatalf("Should find 2 tags after Replace") 225 } 226 227 DB.Model(&blog2).Association("SharedTags").Find(&tags2) 228 if !compareTags(tags2, []string{"tag5", "tag6"}) { 229 t.Fatalf("Should find 2 tags after Replace") 230 } 231 232 if DB.Model(&blog).Association("SharedTags").Count() != 2 { 233 t.Fatalf("Blog should has three tags after Replace") 234 } 235 236 // Delete 237 DB.Model(&blog).Association("SharedTags").Delete(tag5) 238 var tags3 []Tag 239 DB.Model(&blog).Association("SharedTags").Find(&tags3) 240 if !compareTags(tags3, []string{"tag6"}) { 241 t.Fatalf("Should find 1 tags after Delete") 242 } 243 244 if DB.Model(&blog).Association("SharedTags").Count() != 1 { 245 t.Fatalf("Blog should has three tags after Delete") 246 } 247 248 DB.Model(&blog2).Association("SharedTags").Delete(tag3) 249 var tags4 []Tag 250 DB.Model(&blog).Association("SharedTags").Find(&tags4) 251 if !compareTags(tags4, []string{"tag6"}) { 252 t.Fatalf("Tag should not be deleted when Delete with a unrelated tag") 253 } 254 255 // Clear 256 DB.Model(&blog2).Association("SharedTags").Clear() 257 if DB.Model(&blog).Association("SharedTags").Count() != 0 { 258 t.Fatalf("All tags should be cleared") 259 } 260} 261 262func TestManyToManyWithCustomizedForeignKeys2(t *testing.T) { 263 if name := DB.Dialector.Name(); name == "sqlite" || name == "sqlserver" { 264 t.Skip("skip sqlite, sqlserver due to it doesn't support multiple primary keys with auto increment") 265 } 266 267 if name := DB.Dialector.Name(); name == "postgres" { 268 t.Skip("skip postgres due to it only allow unique constraint matching given keys") 269 } 270 271 DB.Migrator().DropTable(&Blog{}, &Tag{}, "blog_tags", "locale_blog_tags", "shared_blog_tags") 272 if err := DB.AutoMigrate(&Blog{}, &Tag{}); err != nil { 273 t.Fatalf("Failed to auto migrate, got error: %v", err) 274 } 275 276 blog := Blog{ 277 Locale: "ZH", 278 Subject: "subject", 279 Body: "body", 280 LocaleTags: []Tag{ 281 {Locale: "ZH", Value: "tag1"}, 282 {Locale: "ZH", Value: "tag2"}, 283 }, 284 } 285 DB.Save(&blog) 286 287 blog2 := Blog{ 288 ID: blog.ID, 289 Locale: "EN", 290 } 291 DB.Create(&blog2) 292 293 // Append 294 var tag3 = &Tag{Locale: "ZH", Value: "tag3"} 295 DB.Model(&blog).Association("LocaleTags").Append([]*Tag{tag3}) 296 if !compareTags(blog.LocaleTags, []string{"tag1", "tag2", "tag3"}) { 297 t.Fatalf("Blog should has three tags after Append") 298 } 299 300 if DB.Model(&blog).Association("LocaleTags").Count() != 3 { 301 t.Fatalf("Blog should has three tags after Append") 302 } 303 304 if DB.Model(&blog2).Association("LocaleTags").Count() != 0 { 305 t.Fatalf("EN Blog should has 0 tags after ZH Blog Append") 306 } 307 308 var tags []Tag 309 DB.Model(&blog).Association("LocaleTags").Find(&tags) 310 if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) { 311 t.Fatalf("Should find 3 tags") 312 } 313 314 DB.Model(&blog2).Association("LocaleTags").Find(&tags) 315 if len(tags) != 0 { 316 t.Fatalf("Should find 0 tags for EN Blog") 317 } 318 319 var blog1 Blog 320 DB.Preload("LocaleTags").Find(&blog1, "locale = ? AND id = ?", "ZH", blog.ID) 321 if !compareTags(blog1.LocaleTags, []string{"tag1", "tag2", "tag3"}) { 322 t.Fatalf("Preload many2many relations") 323 } 324 325 var tag4 = &Tag{Locale: "ZH", Value: "tag4"} 326 DB.Model(&blog2).Association("LocaleTags").Append(tag4) 327 328 DB.Model(&blog).Association("LocaleTags").Find(&tags) 329 if !compareTags(tags, []string{"tag1", "tag2", "tag3"}) { 330 t.Fatalf("Should find 3 tags for EN Blog") 331 } 332 333 DB.Model(&blog2).Association("LocaleTags").Find(&tags) 334 if !compareTags(tags, []string{"tag4"}) { 335 t.Fatalf("Should find 1 tags for EN Blog") 336 } 337 338 // Replace 339 var tag5 = &Tag{Locale: "ZH", Value: "tag5"} 340 var tag6 = &Tag{Locale: "ZH", Value: "tag6"} 341 DB.Model(&blog2).Association("LocaleTags").Replace(tag5, tag6) 342 343 var tags2 []Tag 344 DB.Model(&blog).Association("LocaleTags").Find(&tags2) 345 if !compareTags(tags2, []string{"tag1", "tag2", "tag3"}) { 346 t.Fatalf("CN Blog's tags should not be changed after EN Blog Replace") 347 } 348 349 var blog11 Blog 350 DB.Preload("LocaleTags").First(&blog11, "id = ? AND locale = ?", blog.ID, blog.Locale) 351 if !compareTags(blog11.LocaleTags, []string{"tag1", "tag2", "tag3"}) { 352 t.Fatalf("CN Blog's tags should not be changed after EN Blog Replace") 353 } 354 355 DB.Model(&blog2).Association("LocaleTags").Find(&tags2) 356 if !compareTags(tags2, []string{"tag5", "tag6"}) { 357 t.Fatalf("Should find 2 tags after Replace") 358 } 359 360 var blog21 Blog 361 DB.Preload("LocaleTags").First(&blog21, "id = ? AND locale = ?", blog2.ID, blog2.Locale) 362 if !compareTags(blog21.LocaleTags, []string{"tag5", "tag6"}) { 363 t.Fatalf("EN Blog's tags should be changed after Replace") 364 } 365 366 if DB.Model(&blog).Association("LocaleTags").Count() != 3 { 367 t.Fatalf("ZH Blog should has three tags after Replace") 368 } 369 370 if DB.Model(&blog2).Association("LocaleTags").Count() != 2 { 371 t.Fatalf("EN Blog should has two tags after Replace") 372 } 373 374 // Delete 375 DB.Model(&blog).Association("LocaleTags").Delete(tag5) 376 377 if DB.Model(&blog).Association("LocaleTags").Count() != 3 { 378 t.Fatalf("ZH Blog should has three tags after Delete with EN's tag") 379 } 380 381 if DB.Model(&blog2).Association("LocaleTags").Count() != 2 { 382 t.Fatalf("EN Blog should has two tags after ZH Blog Delete with EN's tag") 383 } 384 385 DB.Model(&blog2).Association("LocaleTags").Delete(tag5) 386 387 if DB.Model(&blog).Association("LocaleTags").Count() != 3 { 388 t.Fatalf("ZH Blog should has three tags after EN Blog Delete with EN's tag") 389 } 390 391 if DB.Model(&blog2).Association("LocaleTags").Count() != 1 { 392 t.Fatalf("EN Blog should has 1 tags after EN Blog Delete with EN's tag") 393 } 394 395 // Clear 396 DB.Model(&blog2).Association("LocaleTags").Clear() 397 if DB.Model(&blog).Association("LocaleTags").Count() != 3 { 398 t.Fatalf("ZH Blog's tags should not be cleared when clear EN Blog's tags") 399 } 400 401 if DB.Model(&blog2).Association("LocaleTags").Count() != 0 { 402 t.Fatalf("EN Blog's tags should be cleared when clear EN Blog's tags") 403 } 404 405 DB.Model(&blog).Association("LocaleTags").Clear() 406 if DB.Model(&blog).Association("LocaleTags").Count() != 0 { 407 t.Fatalf("ZH Blog's tags should be cleared when clear ZH Blog's tags") 408 } 409 410 if DB.Model(&blog2).Association("LocaleTags").Count() != 0 { 411 t.Fatalf("EN Blog's tags should be cleared") 412 } 413} 414 415func TestCompositePrimaryKeysAssociations(t *testing.T) { 416 type Label struct { 417 BookID *uint `gorm:"primarykey"` 418 Name string `gorm:"primarykey"` 419 Value string 420 } 421 422 type Book struct { 423 ID int 424 Name string 425 Labels []Label 426 } 427 428 DB.Migrator().DropTable(&Label{}, &Book{}) 429 if err := DB.AutoMigrate(&Label{}, &Book{}); err != nil { 430 t.Fatalf("failed to migrate") 431 } 432 433 book := Book{ 434 Name: "my book", 435 Labels: []Label{ 436 {Name: "region", Value: "emea"}, 437 }, 438 } 439 440 DB.Create(&book) 441 442 var result Book 443 if err := DB.Preload("Labels").First(&result, book.ID).Error; err != nil { 444 t.Fatalf("failed to preload, got error %v", err) 445 } 446 447 AssertEqual(t, book, result) 448} 449