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
566 lines
11 KiB
Markdown
566 lines
11 KiB
Markdown
# 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:
|
|
|
|
```env
|
|
# 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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
class User extends Model {
|
|
getFullName() {
|
|
return this.full_name;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Assoziationen
|
|
|
|
Models sind miteinander verknüpft:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
const latest = await Worklog.findAll({
|
|
where: { user_id: 1 },
|
|
order: [['tstamp', 'DESC']],
|
|
limit: 10,
|
|
offset: 0
|
|
});
|
|
```
|
|
|
|
### Aggregationen
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```javascript
|
|
// ✅ Gut
|
|
const { Worklog } = database.getModels();
|
|
const worklogs = await Worklog.findAll();
|
|
|
|
// ❌ Nicht
|
|
const Worklog = require('../models/Worklog');
|
|
```
|
|
|
|
### 2. Eager Loading für Beziehungen
|
|
|
|
```javascript
|
|
// ✅ 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
|
|
|
|
```javascript
|
|
// ✅ 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
|
|
|
|
```javascript
|
|
// Für komplexe Aggregationen sind Raw Queries oft schneller
|
|
const stats = await sequelize.query(complexSQL, {
|
|
replacements: { userId },
|
|
type: QueryTypes.SELECT
|
|
});
|
|
```
|
|
|
|
## Debugging
|
|
|
|
### SQL-Queries anzeigen
|
|
|
|
```env
|
|
# 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
|
|
|
|
```javascript
|
|
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"
|
|
|
|
```javascript
|
|
// Sicherstellen, dass database.initialize() aufgerufen wurde
|
|
await database.initialize();
|
|
```
|
|
|
|
### "Model not found"
|
|
|
|
```javascript
|
|
// Models werden in database.js initialisiert
|
|
// Prüfen Sie, ob Model in initializeModels() geladen wird
|
|
```
|
|
|
|
### Timezone-Probleme
|
|
|
|
```env
|
|
# In .env die richtige Timezone setzen
|
|
DB_TIMEZONE=+01:00
|
|
```
|
|
|
|
## Migration von Raw SQL
|
|
|
|
### Vorher (Raw SQL):
|
|
|
|
```javascript
|
|
const [rows] = await db.execute(
|
|
'SELECT * FROM user WHERE id = ?',
|
|
[userId]
|
|
);
|
|
```
|
|
|
|
### Nachher (Sequelize):
|
|
|
|
```javascript
|
|
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!
|
|
|
|
|
|
|