Skip to content

Commit 5b595f8

Browse files
authored
Split GamePhotoSubjects into their own DB table (#1046)
Makes working on photos easier and less chaotic, now without making photo fetch requests slower.
2 parents 595ecdf + 6c61962 commit 5b595f8

14 files changed

Lines changed: 445 additions & 180 deletions

File tree

Refresh.Core/Extensions/PhotoExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static SerializedPhoto FromGamePhoto(GamePhoto photo, DataContext dataCon
1919
MediumHash = dataContext.Database.GetAssetFromHash(photo.MediumAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.MediumAsset.AssetHash,
2020
SmallHash = dataContext.Database.GetAssetFromHash(photo.SmallAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.SmallAsset.AssetHash,
2121
PlanHash = photo.PlanHash,
22-
PhotoSubjects = new List<SerializedPhotoSubject>(photo.Subjects.Count),
22+
PhotoSubjects = [],
2323
};
2424

2525
foreach (GamePhotoSubject subject in photo.Subjects)

Refresh.Database/GameDatabaseContext.Photos.cs

Lines changed: 143 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Refresh.Database.Models.Photos;
66
using Refresh.Database.Query;
77
using Refresh.Database.Helpers;
8+
using Bunkum.Core;
89

910
namespace Refresh.Database;
1011

@@ -19,14 +20,21 @@ public partial class GameDatabaseContext // Photos
1920
.Include(p => p.Level)
2021
.Include(p => p.Level!.Publisher)
2122
.Include(p => p.Level!.Publisher!.Statistics)
22-
.Include(p => p.Subject1User)
23-
.Include(p => p.Subject1User!.Statistics)
24-
.Include(p => p.Subject2User)
25-
.Include(p => p.Subject2User!.Statistics)
26-
.Include(p => p.Subject3User)
27-
.Include(p => p.Subject3User!.Statistics)
28-
.Include(p => p.Subject4User)
29-
.Include(p => p.Subject4User!.Statistics);
23+
.Include(p => p.Subjects.OrderBy(s => s.PlayerId));
24+
25+
private IQueryable<GamePhotoSubject> GamePhotoSubjectsIncluded => this.GamePhotoSubjects
26+
.Include(p => p.User)
27+
.Include(p => p.User!.Statistics)
28+
.Include(p => p.Photo)
29+
.Include(p => p.Photo!.Publisher)
30+
.Include(p => p.Photo!.Publisher.Statistics)
31+
.Include(p => p.Photo!.Level)
32+
.Include(p => p.Photo!.Level!.Publisher)
33+
.Include(p => p.Photo!.Level!.Publisher!.Statistics)
34+
.Include(p => p.Photo!.LargeAsset)
35+
.Include(p => p.Photo!.MediumAsset)
36+
.Include(p => p.Photo!.SmallAsset)
37+
.Include(p => p.Photo!.Subjects.OrderBy(s => s.PlayerId));
3038

3139
public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable<IPhotoUploadSubject> subjects, GameUser publisher, GameLevel? level)
3240
{
@@ -47,29 +55,6 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable<IPhotoUploadSubject
4755
PublishedAt = this._time.Now,
4856
};
4957

50-
List<GamePhotoSubject> gameSubjects = new(subjects.Count());
51-
foreach (IPhotoUploadSubject subject in subjects)
52-
{
53-
GameUser? subjectUser = null;
54-
55-
if (!string.IsNullOrEmpty(subject.Username))
56-
subjectUser = this.GetUserByUsername(subject.Username);
57-
58-
float[] bounds = PhotoHelper.ParseBoundsList(subject.BoundsList);
59-
60-
gameSubjects.Add(new GamePhotoSubject(subjectUser, subject.DisplayName, bounds));
61-
62-
if (subjectUser != null)
63-
{
64-
this.WriteEnsuringStatistics(subjectUser, () =>
65-
{
66-
subjectUser.Statistics!.PhotosWithUserCount++;
67-
});
68-
}
69-
}
70-
71-
newPhoto.Subjects = gameSubjects;
72-
7358
this.WriteEnsuringStatistics(publisher, () =>
7459
{
7560
this.GamePhotos.Add(newPhoto);
@@ -87,22 +72,124 @@ public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable<IPhotoUploadSubject
8772
}
8873
});
8974
}
75+
76+
List<IPhotoUploadSubject> subjectsList = subjects.ToList();
77+
List<GamePhotoSubject> finalSubjectsList = [];
78+
79+
// Take care of subjects after saving the photo itself to keep the photo, even if its subjects are malformed
80+
for (int i = 0; i < subjectsList.Count; i++)
81+
{
82+
IPhotoUploadSubject subject = subjectsList[i];
83+
84+
float[] bounds = new float[PhotoHelper.SubjectBoundaryCount];
85+
try
86+
{
87+
bounds = PhotoHelper.ParseBoundsList(subject.BoundsList);
88+
}
89+
catch (Exception ex)
90+
{
91+
this._logger.LogWarning(BunkumCategory.UserPhotos, $"Could not parse {subject.DisplayName}'s photo bounds: {ex.GetType()} - {ex.Message}");
92+
}
93+
94+
GameUser? subjectUser = string.IsNullOrWhiteSpace(subject.Username) ? null : this.GetUserByUsername(subject.Username);
95+
finalSubjectsList.Add(new()
96+
{
97+
Photo = newPhoto,
98+
User = subjectUser,
99+
DisplayName = subject.DisplayName,
100+
PlayerId = i + 1, // Player number 1 - 4
101+
Bounds = bounds
102+
});
103+
104+
if (subjectUser != null)
105+
{
106+
this.WriteEnsuringStatistics(subjectUser, () =>
107+
{
108+
subjectUser.Statistics!.PhotosWithUserCount++;
109+
});
110+
}
111+
}
112+
113+
this.GamePhotoSubjects.AddRange(finalSubjectsList);
114+
this.SaveChanges();
90115

91116
this.CreatePhotoUploadEvent(publisher, newPhoto);
117+
118+
newPhoto.Subjects = finalSubjectsList.ToList();
92119
return newPhoto;
93120
}
94121

122+
/// <remarks>
123+
/// Migration only!!
124+
/// </remarks>
125+
public void MigratePhotoSubjects(GamePhoto photo, bool saveChanges)
126+
{
127+
List<GamePhotoSubject> subjects = [];
128+
129+
#pragma warning disable CS0618 // obsoletion
130+
131+
// If DisplayName is not null, there is a subject in that spot
132+
if (photo.Subject1DisplayName != null)
133+
{
134+
subjects.Add(new()
135+
{
136+
Photo = photo,
137+
PlayerId = 1,
138+
DisplayName = photo.Subject1DisplayName,
139+
User = photo.Subject1User,
140+
Bounds = photo.Subject1Bounds,
141+
});
142+
}
143+
144+
if (photo.Subject2DisplayName != null)
145+
{
146+
subjects.Add(new()
147+
{
148+
Photo = photo,
149+
PlayerId = 2,
150+
DisplayName = photo.Subject2DisplayName,
151+
User = photo.Subject2User,
152+
Bounds = photo.Subject2Bounds,
153+
});
154+
}
155+
156+
if (photo.Subject3DisplayName != null)
157+
{
158+
subjects.Add(new()
159+
{
160+
Photo = photo,
161+
PlayerId = 3,
162+
DisplayName = photo.Subject3DisplayName,
163+
User = photo.Subject3User,
164+
Bounds = photo.Subject3Bounds,
165+
});
166+
}
167+
168+
if (photo.Subject4DisplayName != null)
169+
{
170+
subjects.Add(new()
171+
{
172+
Photo = photo,
173+
PlayerId = 4,
174+
DisplayName = photo.Subject4DisplayName,
175+
User = photo.Subject4User,
176+
Bounds = photo.Subject4Bounds,
177+
});
178+
}
179+
#pragma warning restore CS0618
180+
181+
this.GamePhotoSubjects.AddRange(subjects);
182+
if (saveChanges) this.SaveChanges();
183+
}
184+
95185
public void RemovePhoto(GamePhoto photo)
96186
{
97-
foreach (GamePhotoSubject subject in photo.Subjects)
187+
foreach (GameUser subjectUser in this.GetUsersInPhoto(photo).ToArray())
98188
{
99-
if (subject.User != null)
189+
this.WriteEnsuringStatistics(subjectUser, () =>
100190
{
101-
this.WriteEnsuringStatistics(subject.User, () =>
102-
{
103-
subject.User.Statistics!.PhotosWithUserCount--;
104-
});
105-
}
191+
subjectUser.Statistics!.PhotosWithUserCount--;
192+
});
106193
}
107194

108195
if (photo.Level != null)
@@ -124,6 +211,9 @@ public void RemovePhoto(GamePhoto photo)
124211

125212
// Remove all events referencing the photo
126213
this.Events.RemoveRange(photoEvents);
214+
215+
// Remove all subjects
216+
this.GamePhotoSubjects.RemoveRange(s => s.PhotoId == photo.PhotoId);
127217

128218
// Remove the photo
129219
this.GamePhotos.Remove(photo);
@@ -132,6 +222,17 @@ public void RemovePhoto(GamePhoto photo)
132222
});
133223
}
134224

225+
public IQueryable<GamePhotoSubject> GetSubjectsInPhoto(GamePhoto photo)
226+
=> this.GamePhotoSubjectsIncluded
227+
.Where(s => s.PhotoId == photo.PhotoId)
228+
.OrderBy(s => s.PlayerId);
229+
230+
public IQueryable<GameUser> GetUsersInPhoto(GamePhoto photo)
231+
=> this.GetSubjectsInPhoto(photo)
232+
.Where(s => s.User != null)
233+
.OrderBy(s => s.PlayerId)
234+
.Select(s => s.User!);
235+
135236
public int GetTotalPhotoCount() => this.GamePhotos.Count();
136237

137238
[Pure]
@@ -155,14 +256,14 @@ public int GetTotalPhotosByUser(GameUser user)
155256

156257
[Pure]
157258
public DatabaseList<GamePhoto> GetPhotosWithUser(GameUser user, int count, int skip) =>
158-
new(this.GamePhotosIncluded
159-
.Where(p => p.Subject1UserId == user.UserId || p.Subject2UserId == user.UserId || p.Subject3UserId == user.UserId || p.Subject4UserId == user.UserId)
259+
new(this.GamePhotoSubjectsIncluded
260+
.Where(s => s.UserId == user.UserId)
261+
.Select(s => s.Photo)
160262
.OrderByDescending(p => p.TakenAt), skip, count);
161263

162264
[Pure]
163265
public int GetTotalPhotosWithUser(GameUser user)
164-
=> this.GamePhotos
165-
.Count(p => p.Subject1UserId == user.UserId || p.Subject2UserId == user.UserId || p.Subject3UserId == user.UserId || p.Subject4UserId == user.UserId);
266+
=> this.GamePhotoSubjects.Count(s => s.UserId == user.UserId);
166267

167268
[Pure]
168269
public DatabaseList<GamePhoto> GetPhotosInLevel(GameLevel level, int count, int skip)

Refresh.Database/GameDatabaseContext.Users.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,11 @@ public void DeleteUser(GameUser user)
401401
user.PsnAuthenticationAllowed = false;
402402
user.RpcnAuthenticationAllowed = false;
403403

404-
foreach (GamePhoto photo in this.GetPhotosWithUser(user, int.MaxValue, 0).Items)
405-
foreach (GamePhotoSubject subject in photo.Subjects.Where(s => s.User?.UserId == user.UserId))
406-
subject.User = null;
407-
404+
foreach (GamePhotoSubject subject in this.GamePhotoSubjects.Where(s => s.UserId == user.UserId).ToList())
405+
{
406+
subject.UserId = null;
407+
}
408+
408409
this.FavouriteLevelRelations.RemoveRange(r => r.User == user);
409410
this.FavouriteUserRelations.RemoveRange(r => r.UserToFavourite == user);
410411
this.FavouriteUserRelations.RemoveRange(r => r.UserFavouriting == user);

Refresh.Database/GameDatabaseContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext
5353
internal DbSet<GameAsset> GameAssets { get; set; }
5454
internal DbSet<GameNotification> GameNotifications { get; set; }
5555
internal DbSet<GamePhoto> GamePhotos { get; set; }
56+
internal DbSet<GamePhotoSubject> GamePhotoSubjects { get; set; }
5657
internal DbSet<GameIpVerificationRequest> GameIpVerificationRequests { get; set; }
5758
internal DbSet<GameAnnouncement> GameAnnouncements { get; set; }
5859
internal DbSet<QueuedRegistration> QueuedRegistrations { get; set; }

Refresh.Database/Helpers/PhotoHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static float[] ParseBoundsList(string input)
2222
string boundaryStr = boundsStr[i];
2323

2424
if (!float.TryParse(boundaryStr, NumberFormatInfo.InvariantInfo, out float f))
25-
throw new FormatException($"Boundary {boundaryStr} ({i+1}/{SubjectBoundaryCount}) is not a float");
25+
throw new FormatException($"Boundary '{boundaryStr}' ({i+1}/{SubjectBoundaryCount}) is not a float");
2626

2727
boundsParsed[i] = f;
2828
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace Refresh.Database.Migrations
7+
{
8+
/// <inheritdoc />
9+
[DbContext(typeof(GameDatabaseContext))]
10+
[Migration("20260309162631_SplitGamePhotoSubjects")]
11+
public partial class SplitGamePhotoSubjects : Migration
12+
{
13+
/// <inheritdoc />
14+
protected override void Up(MigrationBuilder migrationBuilder)
15+
{
16+
migrationBuilder.CreateTable(
17+
name: "GamePhotoSubjects",
18+
columns: table => new
19+
{
20+
PhotoId = table.Column<int>(type: "integer", nullable: false),
21+
PlayerId = table.Column<int>(type: "integer", nullable: false),
22+
UserId = table.Column<string>(type: "text", nullable: true),
23+
DisplayName = table.Column<string>(type: "text", nullable: false),
24+
Bounds = table.Column<float[]>(type: "real[]", nullable: false)
25+
},
26+
constraints: table =>
27+
{
28+
table.PrimaryKey("PK_GamePhotoSubjects", x => new { x.PhotoId, x.PlayerId });
29+
table.ForeignKey(
30+
name: "FK_GamePhotoSubjects_GamePhotos_PhotoId",
31+
column: x => x.PhotoId,
32+
principalTable: "GamePhotos",
33+
principalColumn: "PhotoId",
34+
onDelete: ReferentialAction.Cascade);
35+
table.ForeignKey(
36+
name: "FK_GamePhotoSubjects_GameUsers_UserId",
37+
column: x => x.UserId,
38+
principalTable: "GameUsers",
39+
principalColumn: "UserId");
40+
});
41+
42+
migrationBuilder.CreateIndex(
43+
name: "IX_GamePhotoSubjects_UserId",
44+
table: "GamePhotoSubjects",
45+
column: "UserId");
46+
}
47+
48+
/// <inheritdoc />
49+
protected override void Down(MigrationBuilder migrationBuilder)
50+
{
51+
migrationBuilder.DropTable(
52+
name: "GamePhotoSubjects");
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)