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
This commit is contained in:
565
backend/SEQUELIZE.md
Normal file
565
backend/SEQUELIZE.md
Normal file
@@ -0,0 +1,565 @@
|
||||
# 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!
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user