Skip to content

Commit 754f675

Browse files
PathToLifekirill-dev-pro
authored andcommitted
feat: improve performance for GET /api/album & /api/album/:id (immich-app#17124)
* fix(server) optimize number of sql calls for GET /api/albums remove unnecessary join for getMetadataForIds remove separate call to getLastUpdatedAssetForAlbumId * fix(server) remove unnecessary getLastUpdatedAssetForAlbumId call for GET /api/album/:id also remove getLastUpdatedAssetForAlbumId query as it is no longer referenced * fix(server): correct lastModifiedAssetTimestamp return type + formatting and typing * chore(server): address type issue with tests found via npm:check tests & lint still pass before this commit.
1 parent b0eb8ce commit 754f675

File tree

6 files changed

+71
-51
lines changed

6 files changed

+71
-51
lines changed

server/src/queries/album.repository.sql

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,19 +201,23 @@ order by
201201

202202
-- AlbumRepository.getMetadataForIds
203203
select
204-
"albums"."id" as "albumId",
205-
min("assets"."localDateTime") as "startDate",
206-
max("assets"."localDateTime") as "endDate",
204+
"album_assets"."albumsId" as "albumId",
205+
min(
206+
("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date
207+
) as "startDate",
208+
max(
209+
("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date
210+
) as "endDate",
211+
max("assets"."updatedAt") as "lastModifiedAssetTimestamp",
207212
count("assets"."id")::int as "assetCount"
208213
from
209-
"albums"
210-
inner join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
211-
inner join "assets" on "assets"."id" = "album_assets"."assetsId"
214+
"assets"
215+
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
212216
where
213-
"albums"."id" in ($1)
217+
"album_assets"."albumsId" in ($1)
214218
and "assets"."deletedAt" is null
215219
group by
216-
"albums"."id"
220+
"album_assets"."albumsId"
217221

218222
-- AlbumRepository.getOwned
219223
select

server/src/repositories/album.repository.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface AlbumAssetCount {
1212
assetCount: number;
1313
startDate: Date | null;
1414
endDate: Date | null;
15+
lastModifiedAssetTimestamp: Date | null;
1516
}
1617

1718
export interface AlbumInfoOptions {
@@ -132,18 +133,21 @@ export class AlbumRepository {
132133
return [];
133134
}
134135

135-
return this.db
136-
.selectFrom('albums')
137-
.innerJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
138-
.innerJoin('assets', 'assets.id', 'album_assets.assetsId')
139-
.select('albums.id as albumId')
140-
.select((eb) => eb.fn.min('assets.localDateTime').as('startDate'))
141-
.select((eb) => eb.fn.max('assets.localDateTime').as('endDate'))
142-
.select((eb) => sql<number>`${eb.fn.count('assets.id')}::int`.as('assetCount'))
143-
.where('albums.id', 'in', ids)
144-
.where('assets.deletedAt', 'is', null)
145-
.groupBy('albums.id')
146-
.execute();
136+
return (
137+
this.db
138+
.selectFrom('assets')
139+
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
140+
.select('album_assets.albumsId as albumId')
141+
.select((eb) => eb.fn.min(sql<Date>`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate'))
142+
.select((eb) => eb.fn.max(sql<Date>`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('endDate'))
143+
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
144+
.select((eb) => eb.fn.max('assets.updatedAt').as('lastModifiedAssetTimestamp'))
145+
.select((eb) => sql<number>`${eb.fn.count('assets.id')}::int`.as('assetCount'))
146+
.where('album_assets.albumsId', 'in', ids)
147+
.where('assets.deletedAt', 'is', null)
148+
.groupBy('album_assets.albumsId')
149+
.execute()
150+
);
147151
}
148152

149153
@GenerateSql({ params: [DummyValue.UUID] })

server/src/repositories/asset.repository.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -728,17 +728,6 @@ export class AssetRepository {
728728
return paginationHelper(items as any as AssetEntity[], pagination.take);
729729
}
730730

731-
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | undefined> {
732-
return this.db
733-
.selectFrom('assets')
734-
.selectAll('assets')
735-
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
736-
.where('albums_assets_assets.albumsId', '=', asUuid(albumId))
737-
.orderBy('updatedAt', 'desc')
738-
.limit(1)
739-
.executeTakeFirst() as Promise<AssetEntity | undefined>;
740-
}
741-
742731
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
743732
return this.db
744733
.selectFrom('assets')

server/src/services/album.service.spec.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,20 @@ describe(AlbumService.name, () => {
4141
it('gets list of albums for auth user', async () => {
4242
mocks.album.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
4343
mocks.album.getMetadataForIds.mockResolvedValue([
44-
{ albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null },
45-
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null },
44+
{
45+
albumId: albumStub.empty.id,
46+
assetCount: 0,
47+
startDate: null,
48+
endDate: null,
49+
lastModifiedAssetTimestamp: null,
50+
},
51+
{
52+
albumId: albumStub.sharedWithUser.id,
53+
assetCount: 0,
54+
startDate: null,
55+
endDate: null,
56+
lastModifiedAssetTimestamp: null,
57+
},
4658
]);
4759

4860
const result = await sut.getAll(authStub.admin, {});
@@ -59,6 +71,7 @@ describe(AlbumService.name, () => {
5971
assetCount: 1,
6072
startDate: new Date('1970-01-01'),
6173
endDate: new Date('1970-01-01'),
74+
lastModifiedAssetTimestamp: new Date('1970-01-01'),
6275
},
6376
]);
6477

@@ -71,7 +84,13 @@ describe(AlbumService.name, () => {
7184
it('gets list of albums that are shared', async () => {
7285
mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]);
7386
mocks.album.getMetadataForIds.mockResolvedValue([
74-
{ albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: null, endDate: null },
87+
{
88+
albumId: albumStub.sharedWithUser.id,
89+
assetCount: 0,
90+
startDate: null,
91+
endDate: null,
92+
lastModifiedAssetTimestamp: null,
93+
},
7594
]);
7695

7796
const result = await sut.getAll(authStub.admin, { shared: true });
@@ -83,7 +102,13 @@ describe(AlbumService.name, () => {
83102
it('gets list of albums that are NOT shared', async () => {
84103
mocks.album.getNotShared.mockResolvedValue([albumStub.empty]);
85104
mocks.album.getMetadataForIds.mockResolvedValue([
86-
{ albumId: albumStub.empty.id, assetCount: 0, startDate: null, endDate: null },
105+
{
106+
albumId: albumStub.empty.id,
107+
assetCount: 0,
108+
startDate: null,
109+
endDate: null,
110+
lastModifiedAssetTimestamp: null,
111+
},
87112
]);
88113

89114
const result = await sut.getAll(authStub.admin, { shared: false });
@@ -101,6 +126,7 @@ describe(AlbumService.name, () => {
101126
assetCount: 1,
102127
startDate: new Date('1970-01-01'),
103128
endDate: new Date('1970-01-01'),
129+
lastModifiedAssetTimestamp: new Date('1970-01-01'),
104130
},
105131
]);
106132

@@ -447,6 +473,7 @@ describe(AlbumService.name, () => {
447473
assetCount: 1,
448474
startDate: new Date('1970-01-01'),
449475
endDate: new Date('1970-01-01'),
476+
lastModifiedAssetTimestamp: new Date('1970-01-01'),
450477
},
451478
]);
452479

@@ -468,6 +495,7 @@ describe(AlbumService.name, () => {
468495
assetCount: 1,
469496
startDate: new Date('1970-01-01'),
470497
endDate: new Date('1970-01-01'),
498+
lastModifiedAssetTimestamp: new Date('1970-01-01'),
471499
},
472500
]);
473501

@@ -489,6 +517,7 @@ describe(AlbumService.name, () => {
489517
assetCount: 1,
490518
startDate: new Date('1970-01-01'),
491519
endDate: new Date('1970-01-01'),
520+
lastModifiedAssetTimestamp: new Date('1970-01-01'),
492521
},
493522
]);
494523

server/src/services/album.service.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,15 @@ export class AlbumService extends BaseService {
5858
albumMetadata[metadata.albumId] = metadata;
5959
}
6060

61-
return Promise.all(
62-
albums.map(async (album) => {
63-
const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
64-
return {
65-
...mapAlbumWithoutAssets(album),
66-
sharedLinks: undefined,
67-
startDate: albumMetadata[album.id]?.startDate ?? undefined,
68-
endDate: albumMetadata[album.id]?.endDate ?? undefined,
69-
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
70-
lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
71-
};
72-
}),
73-
);
61+
return albums.map((album) => ({
62+
...mapAlbumWithoutAssets(album),
63+
sharedLinks: undefined,
64+
startDate: albumMetadata[album.id]?.startDate ?? undefined,
65+
endDate: albumMetadata[album.id]?.endDate ?? undefined,
66+
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
67+
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
68+
lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
69+
}));
7470
}
7571

7672
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
@@ -79,14 +75,13 @@ export class AlbumService extends BaseService {
7975
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
8076
const album = await this.findOrFail(id, { withAssets });
8177
const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]);
82-
const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
8378

8479
return {
8580
...mapAlbum(album, withAssets, auth),
8681
startDate: albumMetadataForIds?.startDate ?? undefined,
8782
endDate: albumMetadataForIds?.endDate ?? undefined,
8883
assetCount: albumMetadataForIds?.assetCount ?? 0,
89-
lastModifiedAssetTimestamp: lastModifiedAsset?.updatedAt,
84+
lastModifiedAssetTimestamp: albumMetadataForIds?.lastModifiedAssetTimestamp ?? undefined,
9085
};
9186
}
9287

server/test/repositories/asset.repository.mock.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
2121
getByChecksums: vitest.fn(),
2222
getUploadAssetIdByChecksum: vitest.fn(),
2323
getRandom: vitest.fn(),
24-
getLastUpdatedAssetForAlbumId: vitest.fn(),
2524
getAll: vitest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
2625
getAllByDeviceId: vitest.fn(),
2726
getLivePhotoCount: vitest.fn(),

0 commit comments

Comments
 (0)