Files
stechuhr3/backend/SEQUELIZE.md
Torsten Schulz (local) e95bb4cb76 Initial commit: TimeClock v3 - Node.js/Vue.js Zeiterfassung
Features:
- Backend: Node.js/Express mit MySQL/MariaDB
- Frontend: Vue.js 3 mit Composition API
- UTC-Zeithandling für korrekte Zeiterfassung
- Timewish-basierte Überstundenberechnung
- Wochenübersicht mit Urlaubs-/Krankheits-/Feiertagshandling
- Bereinigtes Arbeitsende (Generell/Woche)
- Überstunden-Offset für historische Daten
- Fixed Layout mit scrollbarem Content
- Kompakte UI mit grünem Theme
2025-10-17 14:11:28 +02:00

11 KiB

Sequelize ORM Integration

Übersicht

TimeClock v3 verwendet Sequelize als ORM (Object-Relational Mapping) für die MySQL-Datenbank. Dies ermöglicht eine typsichere, objektorientierte Arbeitsweise mit der Datenbank.

Vorteile von Sequelize

Type-Safety - Models definieren Datentypen
Validierung - Automatische Datenvalidierung
Assoziationen - Einfache Beziehungen zwischen Models
Migrations - Versionskontrolle für Datenbankschema
Abstraktion - Datenbank-unabhängiger Code
Query Builder - Typsichere Queries

Konfiguration

.env Datei

Alle Datenbankeinstellungen werden über .env konfiguriert:

# MySQL Datenbank-Verbindung
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=ihr_passwort
DB_NAME=stechuhr2

# Datenbank-Optionen
DB_LOGGING=false          # SQL-Queries in Console loggen (true/false)
DB_TIMEZONE=+01:00        # Timezone für Timestamps
DB_POOL_MAX=10            # Max. Connections im Pool
DB_POOL_MIN=0             # Min. Connections im Pool

Initialisierung

// src/config/database.js
const sequelize = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  process.env.DB_PASSWORD,
  {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    dialect: 'mysql',
    logging: process.env.DB_LOGGING === 'true',
    pool: {
      max: parseInt(process.env.DB_POOL_MAX),
      min: parseInt(process.env.DB_POOL_MIN)
    }
  }
);

Models

Model-Struktur

Jedes Model repräsentiert eine Datenbanktabelle:

// src/models/User.js
const { Model, DataTypes } = require('sequelize');

class User extends Model {
  static initialize(sequelize) {
    User.init(
      {
        id: {
          type: DataTypes.BIGINT,
          primaryKey: true,
          autoIncrement: true
        },
        full_name: {
          type: DataTypes.TEXT,
          allowNull: false
        }
        // ... weitere Felder
      },
      {
        sequelize,
        tableName: 'user',
        timestamps: false
      }
    );
    return User;
  }
}

Verfügbare Models

Model Tabelle Beschreibung
User user Benutzer
Worklog worklog Zeiteinträge (Clock In/Out)
AuthInfo auth_info Authentifizierung
State state Bundesländer/Regionen
WeeklyWorktime weekly_worktime Wochenarbeitszeit
Holiday holiday Feiertage
Vacation vacation Urlaub
Sick sick Krankmeldungen
SickType sick_type Krankmeldungstypen

Model-Features

1. Getter/Setter

// Worklog Model
class Worklog extends Model {
  // State als JSON speichern/laden
  state: {
    type: DataTypes.TEXT,
    get() {
      const rawValue = this.getDataValue('state');
      try {
        return JSON.parse(rawValue);
      } catch {
        return { action: rawValue };
      }
    },
    set(value) {
      if (typeof value === 'object') {
        this.setDataValue('state', JSON.stringify(value));
      }
    }
  }
}

// Verwendung:
worklog.state = { action: 'Clock In', project: 'Website' };
console.log(worklog.state.project); // 'Website'

2. Instance Methods

class Worklog extends Model {
  isClockIn() {
    return this.relatedTo_id === null;
  }
  
  getProject() {
    return this.state.project || 'Allgemein';
  }
}

// Verwendung:
if (worklog.isClockIn()) {
  console.log(`Projekt: ${worklog.getProject()}`);
}

3. Class Methods

class User extends Model {
  getFullName() {
    return this.full_name;
  }
}

Assoziationen

Models sind miteinander verknüpft:

// User → Worklog (1:n)
User.hasMany(Worklog, { foreignKey: 'user_id', as: 'worklogs' });
Worklog.belongsTo(User, { foreignKey: 'user_id', as: 'user' });

// Worklog → Worklog (Clock In/Out)
Worklog.belongsTo(Worklog, { foreignKey: 'relatedTo_id', as: 'relatedTo' });
Worklog.hasOne(Worklog, { foreignKey: 'relatedTo_id', as: 'clockOut' });

// User → AuthInfo (1:1)
User.hasOne(AuthInfo, { foreignKey: 'user_id', as: 'authInfo' });
AuthInfo.belongsTo(User, { foreignKey: 'user_id', as: 'user' });

Include in Queries

// User mit Worklogs laden
const user = await User.findByPk(1, {
  include: [{
    model: Worklog,
    as: 'worklogs',
    limit: 10
  }]
});

console.log(user.worklogs); // Array von Worklog-Instanzen

Repository-Pattern

Repositories verwenden Sequelize Models:

class WorklogRepository {
  async findById(id) {
    const { Worklog } = database.getModels();
    return await Worklog.findByPk(id);
  }

  async create(data) {
    const { Worklog } = database.getModels();
    return await Worklog.create(data);
  }

  async update(id, updates) {
    const { Worklog } = database.getModels();
    const worklog = await Worklog.findByPk(id);
    await worklog.update(updates);
    return worklog;
  }
}

Query-Beispiele

Einfache Queries

// Alle Benutzer
const users = await User.findAll();

// Benutzer nach ID
const user = await User.findByPk(1);

// Erstellen
const newUser = await User.create({
  full_name: 'Max Mustermann',
  daily_hours: 8
});

// Aktualisieren
await user.update({ full_name: 'Max M.' });

// Löschen
await user.destroy();

WHERE-Bedingungen

const { Op } = require('sequelize');

// Einfache WHERE
const worklogs = await Worklog.findAll({
  where: { user_id: 1 }
});

// Operatoren
const recent = await Worklog.findAll({
  where: {
    tstamp: {
      [Op.gte]: new Date('2025-01-01')
    }
  }
});

// Mehrere Bedingungen
const active = await Worklog.findAll({
  where: {
    user_id: 1,
    relatedTo_id: null
  }
});

Order & Limit

const latest = await Worklog.findAll({
  where: { user_id: 1 },
  order: [['tstamp', 'DESC']],
  limit: 10,
  offset: 0
});

Aggregationen

// COUNT
const count = await Worklog.count({
  where: { user_id: 1 }
});

// SUM (mit raw query)
const [result] = await sequelize.query(`
  SELECT SUM(TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp)) as total
  FROM worklog w1
  JOIN worklog w2 ON w2.relatedTo_id = w1.id
  WHERE w1.user_id = ?
`, {
  replacements: [1],
  type: QueryTypes.SELECT
});

Raw Queries

Für komplexe Queries können Raw SQL verwendet werden:

const sequelize = database.getSequelize();

const [results] = await sequelize.query(`
  SELECT 
    w1.id as start_id,
    w2.id as end_id,
    TIMESTAMPDIFF(SECOND, w1.tstamp, w2.tstamp) as duration
  FROM worklog w1
  LEFT JOIN worklog w2 ON w2.relatedTo_id = w1.id
  WHERE w1.user_id = :userId
`, {
  replacements: { userId: 1 },
  type: sequelize.QueryTypes.SELECT
});

Transaktionen

const sequelize = database.getSequelize();

const t = await sequelize.transaction();

try {
  // Clock In erstellen
  const clockIn = await Worklog.create({
    user_id: 1,
    state: { action: 'Clock In' },
    tstamp: new Date()
  }, { transaction: t });

  // Weitere Operationen...
  
  await t.commit();
} catch (error) {
  await t.rollback();
  throw error;
}

Validierung

class User extends Model {
  static initialize(sequelize) {
    User.init({
      email: {
        type: DataTypes.STRING,
        validate: {
          isEmail: true,
          notEmpty: true
        }
      },
      full_name: {
        type: DataTypes.TEXT,
        validate: {
          len: [2, 255]
        }
      },
      daily_hours: {
        type: DataTypes.INTEGER,
        validate: {
          min: 1,
          max: 24
        }
      }
    }, {
      sequelize,
      tableName: 'user'
    });
  }
}

Hooks (Lifecycle Events)

class User extends Model {
  static initialize(sequelize) {
    User.init({ /* ... */ }, {
      sequelize,
      tableName: 'user',
      hooks: {
        beforeCreate: (user, options) => {
          user.last_change = new Date();
        },
        beforeUpdate: (user, options) => {
          user.version += 1;
          user.last_change = new Date();
        }
      }
    });
  }
}

Migrations (Optional)

Für Datenbankschema-Änderungen:

# Sequelize CLI installieren
npm install --save-dev sequelize-cli

# Initialisieren
npx sequelize-cli init

# Migration erstellen
npx sequelize-cli migration:generate --name add-user-field

# Migrations ausführen
npx sequelize-cli db:migrate

# Migrations rückgängig machen
npx sequelize-cli db:migrate:undo

Best Practices

1. Model-Zugriff über Database

// ✅ Gut
const { Worklog } = database.getModels();
const worklogs = await Worklog.findAll();

// ❌ Nicht
const Worklog = require('../models/Worklog');

2. Eager Loading für Beziehungen

// ✅ Gut - Eine Query
const user = await User.findByPk(1, {
  include: [{ model: Worklog, as: 'worklogs' }]
});

// ❌ Nicht - N+1 Problem
const user = await User.findByPk(1);
const worklogs = await Worklog.findAll({ where: { user_id: user.id } });

3. Transactions für kritische Operationen

// ✅ Gut
const t = await sequelize.transaction();
try {
  await operation1({ transaction: t });
  await operation2({ transaction: t });
  await t.commit();
} catch (error) {
  await t.rollback();
}

4. Raw Queries für Performance

// Für komplexe Aggregationen sind Raw Queries oft schneller
const stats = await sequelize.query(complexSQL, {
  replacements: { userId },
  type: QueryTypes.SELECT
});

Debugging

SQL-Queries anzeigen

# In .env
DB_LOGGING=true

Dann werden alle SQL-Queries in der Console geloggt:

Executing (default): SELECT * FROM `user` WHERE `id` = 1;

Model-Daten anzeigen

console.log(user.toJSON());

Performance-Tipps

  1. Indizes nutzen - Models definieren Indizes
  2. Eager Loading - Include statt separate Queries
  3. Pagination - Limit & Offset verwenden
  4. Raw Queries - Für komplexe Aggregationen
  5. Connection Pooling - Bereits konfiguriert

Troubleshooting

"Sequelize not initialized"

// Sicherstellen, dass database.initialize() aufgerufen wurde
await database.initialize();

"Model not found"

// Models werden in database.js initialisiert
// Prüfen Sie, ob Model in initializeModels() geladen wird

Timezone-Probleme

# In .env die richtige Timezone setzen
DB_TIMEZONE=+01:00

Migration von Raw SQL

Vorher (Raw SQL):

const [rows] = await db.execute(
  'SELECT * FROM user WHERE id = ?',
  [userId]
);

Nachher (Sequelize):

const user = await User.findByPk(userId);

Viel einfacher und typsicher! 🎉

Zusammenfassung

Sequelize bietet:

  • Typsichere Models
  • Automatische Validierung
  • Einfache Assoziationen
  • Query Builder
  • .env-basierte Konfiguration
  • Migrations-Support

Perfekt für professionelle Node.js-Anwendungen!