From f487e6d76551be431be9974ad9fd4e539b4f2312 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 12 Jan 2026 11:09:21 +0100 Subject: [PATCH] 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. --- backend/services/falukantService.js | 299 ++++++++++++++++++---------- 1 file changed, 197 insertions(+), 102 deletions(-) diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 6576141..dcc9aba 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -2880,115 +2880,210 @@ class FalukantService extends BaseService { } async getFamily(hashedUserId) { - const user = await this.getFalukantUserByHashedId(hashedUserId); - if (!user) throw new Error('User not found'); - const character = await FalukantCharacter.findOne({ where: { userId: user.id } }); - if (!character) throw new Error('Character not found for this user'); - let relationships = await Relationship.findAll({ - where: { character1Id: character.id }, - attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress'], - include: [ - { - model: FalukantCharacter, as: 'character2', - attributes: ['id', 'birthdate', 'gender', 'moodId'], + const startTime = Date.now(); + const timings = {}; + + try { + // 1. User und Character laden + const step1Start = Date.now(); + const user = await this.getFalukantUserByHashedId(hashedUserId); + if (!user) throw new Error('User not found'); + const character = await FalukantCharacter.findOne({ where: { userId: user.id } }); + if (!character) throw new Error('Character not found for this user'); + 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: [ - { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, - { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }, - { model: CharacterTrait, as: 'traits' }, - { model: Mood, as: 'mood' }, + { + model: FalukantCharacter, as: 'character2', + attributes: ['id', 'birthdate', 'gender', 'moodId', 'firstName', 'lastName', 'titleOfNobility'], + required: false + }, + { model: RelationshipType, as: 'relationshipType', attributes: ['tr'], required: false } ] - }, - { model: RelationshipType, as: 'relationshipType', attributes: ['tr'] } - ] - }); - relationships = relationships.map(r => ({ - createdAt: r.createdAt, - widowFirstName2: r.widowFirstName2, - progress: r.nextStepProgress, - character2: { - id: r.character2.id, - age: calcAge(r.character2.birthdate), - gender: r.character2.gender, - firstName: r.character2.definedFirstName?.name || 'Unknown', - nobleTitle: r.character2.nobleTitle?.labelTr || '', - mood: r.character2.mood, - traits: r.character2.traits - }, - relationshipType: r.relationshipType.tr - })); - const charsWithChildren = await FalukantCharacter.findAll({ - where: { userId: user.id }, - include: [ - { - model: ChildRelation, - as: 'childrenFather', - include: [{ - model: FalukantCharacter, - as: 'child', - include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] - }] - }, - { - model: ChildRelation, - as: 'childrenMother', - include: [{ - model: FalukantCharacter, - as: 'child', - include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }] - }] + }), + FalukantCharacter.findAll({ + where: { userId: user.id }, + attributes: ['id'], + include: [ + { + model: ChildRelation, + as: 'childrenFather', + attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'], + include: [{ + model: FalukantCharacter, + as: 'child', + attributes: ['id', 'birthdate', 'gender', 'firstName'], + required: false + }], + required: false + }, + { + model: ChildRelation, + as: 'childrenMother', + attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'], + include: [{ + model: FalukantCharacter, + as: 'child', + attributes: ['id', 'birthdate', 'gender', 'firstName'], + required: false + }], + required: false + } + ] + }) + ]); + timings.step2_relationships_children = Date.now() - step2Start; + + // 3. Batch-Loading für Relationship-Character-Daten + const step3Start = Date.now(); + const relationshipCharacters = relationshipsRaw + .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, - }); } - } - // Sort children globally by relation createdAt ascending (older first) - children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt)); - const inProgress = ['wooing', 'engaged', 'married']; - const family = { - relationships: relationships.filter(r => inProgress.includes(r.relationshipType)), - lovers: relationships.filter(r => r.relationshipType === 'lover'), - deathPartners: relationships.filter(r => r.relationshipType === 'widowed'), - children: children.map(({ _createdAt, ...rest }) => rest), - possiblePartners: [] - }; - const ownAge = calcAge(character.birthdate); - console.log(`[getFamily] Character age: ${ownAge}, relationships: ${family.relationships.length}`); - if (ownAge >= 12 && family.relationships.length === 0) { - family.possiblePartners = await this.getPossiblePartners(character.id); - console.log(`[getFamily] Found ${family.possiblePartners.length} existing proposals`); - if (family.possiblePartners.length === 0) { - console.log(`[getFamily] Creating new possible partners...`); - await this.createPossiblePartners( - character.id, - character.gender, - character.regionId, - character.titleOfNobility, - ownAge - ); + children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt)); + timings.step5_map_children = Date.now() - step5Start; + + // 6. Family-Objekt erstellen + const step6Start = Date.now(); + const inProgress = ['wooing', 'engaged', 'married']; + const family = { + relationships: relationships.filter(r => inProgress.includes(r.relationshipType)), + lovers: relationships.filter(r => r.relationshipType === 'lover'), + deathPartners: relationships.filter(r => r.relationshipType === 'widowed'), + children: children.map(({ _createdAt, ...rest }) => rest), + possiblePartners: [] + }; + timings.step6_create_family = Date.now() - step6Start; + + // 7. Possible Partners (nur wenn nötig, asynchron) + const step7Start = Date.now(); + const ownAge = calcAge(character.birthdate); + if (ownAge >= 12 && family.relationships.length === 0) { 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 { - console.log(`[getFamily] Skipping partner search: age=${ownAge}, relationships=${family.relationships.length}`); + timings.step7_possible_partners = Date.now() - step7Start; + + 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) {