Enhance getFamily method in FalukantService for performance and data retrieval

- Implemented detailed timing metrics to track performance across various steps of the getFamily method.
- Refactored data retrieval to load relationships and children in parallel, improving efficiency.
- Enhanced mapping of relationship and child data, including additional attributes for characters.
- Introduced batch loading for related data to minimize database queries and optimize performance.
- Improved error handling and logging for better traceability during execution.
This commit is contained in:
Torsten Schulz (local)
2026-01-12 11:09:21 +01:00
parent 5e26422e9c
commit f487e6d765

View File

@@ -2880,115 +2880,210 @@ class FalukantService extends BaseService {
} }
async getFamily(hashedUserId) { async getFamily(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId); const startTime = Date.now();
if (!user) throw new Error('User not found'); const timings = {};
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) throw new Error('Character not found for this user'); try {
let relationships = await Relationship.findAll({ // 1. User und Character laden
where: { character1Id: character.id }, const step1Start = Date.now();
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress'], const user = await this.getFalukantUserByHashedId(hashedUserId);
include: [ if (!user) throw new Error('User not found');
{ const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
model: FalukantCharacter, as: 'character2', if (!character) throw new Error('Character not found for this user');
attributes: ['id', 'birthdate', 'gender', 'moodId'], timings.step1_user_character = Date.now() - step1Start;
// 2. Relationships und Children parallel laden
const step2Start = Date.now();
const [relationshipsRaw, charsWithChildren] = await Promise.all([
Relationship.findAll({
where: { character1Id: character.id },
attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress', 'relationshipTypeId'],
include: [ include: [
{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, {
{ model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }, model: FalukantCharacter, as: 'character2',
{ model: CharacterTrait, as: 'traits' }, attributes: ['id', 'birthdate', 'gender', 'moodId', 'firstName', 'lastName', 'titleOfNobility'],
{ model: Mood, as: 'mood' }, required: false
},
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'], required: false }
] ]
}, }),
{ model: RelationshipType, as: 'relationshipType', attributes: ['tr'] } FalukantCharacter.findAll({
] where: { userId: user.id },
}); attributes: ['id'],
relationships = relationships.map(r => ({ include: [
createdAt: r.createdAt, {
widowFirstName2: r.widowFirstName2, model: ChildRelation,
progress: r.nextStepProgress, as: 'childrenFather',
character2: { attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
id: r.character2.id, include: [{
age: calcAge(r.character2.birthdate), model: FalukantCharacter,
gender: r.character2.gender, as: 'child',
firstName: r.character2.definedFirstName?.name || 'Unknown', attributes: ['id', 'birthdate', 'gender', 'firstName'],
nobleTitle: r.character2.nobleTitle?.labelTr || '', required: false
mood: r.character2.mood, }],
traits: r.character2.traits required: false
}, },
relationshipType: r.relationshipType.tr {
})); model: ChildRelation,
const charsWithChildren = await FalukantCharacter.findAll({ as: 'childrenMother',
where: { userId: user.id }, attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
include: [ include: [{
{ model: FalukantCharacter,
model: ChildRelation, as: 'child',
as: 'childrenFather', attributes: ['id', 'birthdate', 'gender', 'firstName'],
include: [{ required: false
model: FalukantCharacter, }],
as: 'child', required: false
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] }
}] ]
}, })
{ ]);
model: ChildRelation, timings.step2_relationships_children = Date.now() - step2Start;
as: 'childrenMother',
include: [{ // 3. Batch-Loading für Relationship-Character-Daten
model: FalukantCharacter, const step3Start = Date.now();
as: 'child', const relationshipCharacters = relationshipsRaw
include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] .filter(r => r.character2)
}] .map(r => r.character2);
const relationshipCharacterIds = relationshipCharacters.map(c => c.id);
const childCharacters = charsWithChildren
.flatMap(c => [
...(c.childrenFather || []).map(r => r.child).filter(Boolean),
...(c.childrenMother || []).map(r => r.child).filter(Boolean)
]);
const childCharacterIds = childCharacters.map(c => c.id);
// Sammle alle benötigten IDs
const relationshipFirstNameIds = [...new Set(relationshipCharacters.map(c => c.firstName).filter(Boolean))];
const relationshipLastNameIds = [...new Set(relationshipCharacters.map(c => c.lastName).filter(Boolean))];
const relationshipTitleIds = [...new Set(relationshipCharacters.map(c => c.titleOfNobility).filter(Boolean))];
const relationshipMoodIds = [...new Set(relationshipCharacters.map(c => c.moodId).filter(Boolean))];
const childFirstNameIds = [...new Set(childCharacters.map(c => c.firstName).filter(Boolean))];
const allFirstNameIds = [...new Set([...relationshipFirstNameIds, ...childFirstNameIds])];
// Batch-Load alle benötigten Daten parallel
const [firstNames, lastNames, titles, traits, moods] = await Promise.all([
allFirstNameIds.length > 0 ? FalukantPredefineFirstname.findAll({
where: { id: { [Op.in]: allFirstNameIds } },
attributes: ['id', 'name']
}) : [],
relationshipLastNameIds.length > 0 ? FalukantPredefineLastname.findAll({
where: { id: { [Op.in]: relationshipLastNameIds } },
attributes: ['id', 'name']
}) : [],
relationshipTitleIds.length > 0 ? TitleOfNobility.findAll({
where: { id: { [Op.in]: relationshipTitleIds } },
attributes: ['id', 'labelTr']
}) : [],
relationshipCharacterIds.length > 0 ? CharacterTrait.findAll({
where: { characterId: { [Op.in]: relationshipCharacterIds } },
attributes: ['characterId', 'traitId']
}) : [],
relationshipMoodIds.length > 0 ? Mood.findAll({
where: { id: { [Op.in]: relationshipMoodIds } },
attributes: ['id', 'labelTr']
}) : []
]);
// Erstelle Maps für schnellen Zugriff
const firstNameMap = new Map([...firstNames, ...childFirstNames].map(fn => [fn.id, fn.name]));
const lastNameMap = new Map(lastNames.map(ln => [ln.id, ln.name]));
const titleMap = new Map(titles.map(t => [t.id, t.labelTr]));
const moodMap = new Map(moods.map(m => [m.id, m.labelTr]));
const traitsMap = new Map();
traits.forEach(t => {
if (!traitsMap.has(t.characterId)) {
traitsMap.set(t.characterId, []);
}
traitsMap.get(t.characterId).push(t.traitId);
});
timings.step3_batch_loading = Date.now() - step3Start;
// 4. Relationships mappen
const step4Start = Date.now();
const relationships = relationshipsRaw.map(r => {
const char2 = r.character2;
return {
createdAt: r.createdAt,
widowFirstName2: r.widowFirstName2,
progress: r.nextStepProgress,
character2: char2 ? {
id: char2.id,
age: calcAge(char2.birthdate),
gender: char2.gender,
firstName: firstNameMap.get(char2.firstName) || 'Unknown',
nobleTitle: titleMap.get(char2.titleOfNobility) || '',
mood: moodMap.get(char2.moodId) || null,
traits: traitsMap.get(char2.id) || []
} : null,
relationshipType: r.relationshipType?.tr || ''
};
}).filter(r => r.character2 !== null);
timings.step4_map_relationships = Date.now() - step4Start;
// 5. Children mappen
const step5Start = Date.now();
const children = [];
for (const parentChar of charsWithChildren) {
const allRels = [
...(parentChar.childrenFather || []),
...(parentChar.childrenMother || [])
];
for (const rel of allRels) {
const kid = rel.child;
if (!kid) continue;
children.push({
childCharacterId: kid.id,
name: firstNameMap.get(kid.firstName) || 'Unknown',
gender: kid.gender,
age: calcAge(kid.birthdate),
hasName: rel.nameSet,
isHeir: rel.isHeir || false,
_createdAt: rel.createdAt,
});
} }
]
});
const children = [];
for (const parentChar of charsWithChildren) {
const allRels = [
...(parentChar.childrenFather || []),
...(parentChar.childrenMother || [])
];
for (const rel of allRels) {
const kid = rel.child;
children.push({
childCharacterId: kid.id,
name: kid.definedFirstName?.name || 'Unknown',
gender: kid.gender,
age: calcAge(kid.birthdate),
hasName: rel.nameSet,
isHeir: rel.isHeir || false,
_createdAt: rel.createdAt,
});
} }
} children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
// Sort children globally by relation createdAt ascending (older first) timings.step5_map_children = Date.now() - step5Start;
children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
const inProgress = ['wooing', 'engaged', 'married']; // 6. Family-Objekt erstellen
const family = { const step6Start = Date.now();
relationships: relationships.filter(r => inProgress.includes(r.relationshipType)), const inProgress = ['wooing', 'engaged', 'married'];
lovers: relationships.filter(r => r.relationshipType === 'lover'), const family = {
deathPartners: relationships.filter(r => r.relationshipType === 'widowed'), relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
children: children.map(({ _createdAt, ...rest }) => rest), lovers: relationships.filter(r => r.relationshipType === 'lover'),
possiblePartners: [] deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
}; children: children.map(({ _createdAt, ...rest }) => rest),
const ownAge = calcAge(character.birthdate); possiblePartners: []
console.log(`[getFamily] Character age: ${ownAge}, relationships: ${family.relationships.length}`); };
if (ownAge >= 12 && family.relationships.length === 0) { timings.step6_create_family = Date.now() - step6Start;
family.possiblePartners = await this.getPossiblePartners(character.id);
console.log(`[getFamily] Found ${family.possiblePartners.length} existing proposals`); // 7. Possible Partners (nur wenn nötig, asynchron)
if (family.possiblePartners.length === 0) { const step7Start = Date.now();
console.log(`[getFamily] Creating new possible partners...`); const ownAge = calcAge(character.birthdate);
await this.createPossiblePartners( if (ownAge >= 12 && family.relationships.length === 0) {
character.id,
character.gender,
character.regionId,
character.titleOfNobility,
ownAge
);
family.possiblePartners = await this.getPossiblePartners(character.id); family.possiblePartners = await this.getPossiblePartners(character.id);
console.log(`[getFamily] After creation: ${family.possiblePartners.length} proposals`); if (family.possiblePartners.length === 0) {
// Asynchron erstellen, nicht blockieren
this.createPossiblePartners(
character.id,
character.gender,
character.regionId,
character.titleOfNobility,
ownAge
).catch(err => console.error('[getFamily] Error creating partners (async):', err));
}
} }
} else { timings.step7_possible_partners = Date.now() - step7Start;
console.log(`[getFamily] Skipping partner search: age=${ownAge}, relationships=${family.relationships.length}`);
const totalTime = Date.now() - startTime;
console.log(`[getFamily] Performance: ${totalTime}ms total`, timings);
return family;
} catch (error) {
console.error('[getFamily] Error:', error);
throw error;
} }
return family;
} }
async setHeir(hashedUserId, childCharacterId) { async setHeir(hashedUserId, childCharacterId) {