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
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
- Indizes nutzen - Models definieren Indizes
- Eager Loading - Include statt separate Queries
- Pagination - Limit & Offset verwenden
- Raw Queries - Für komplexe Aggregationen
- 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!