🏢 Multi-tenancy - Architektura Wielodostępowa¶
Panel Księgowy używa virtual tenant isolation - wszystkie dane są w jednej bazie PostgreSQL, ale logicznie izolowane przez filtrowanie po team_id.
🎯 Koncepcja¶
Virtual Tenant Isolation¶
Zamiast osobnych baz danych dla każdego zespołu, używamy jednej bazy danych z filtrowaniem po team_id.
Zalety: - ✅ Prostsze zarządzanie (jedna baza) - ✅ Łatwiejsze backup i restore - ✅ Lepsze wykorzystanie zasobów - ✅ Prostsze skalowanie - ✅ Łatwiejsze migracje
Wady: - ⚠️ Wymaga ścisłego filtrowania (ale BaseTeamModel to zapewnia) - ⚠️ Potencjalnie większa baza (ale PostgreSQL to obsługuje)
🔐 Bezpieczeństwo¶
BaseTeamModel¶
Wszystkie modele biznesowe dziedziczą po BaseTeamModel:
from apps.teams.models import BaseTeamModel
class Client(BaseTeamModel):
name = models.CharField(max_length=200)
nip = models.CharField(max_length=10)
# team field jest automatycznie dodany
BaseTeamModel zapewnia:
- Automatyczne pole team (ForeignKey)
- Wymuszenie filtrowania po team
- Izolację danych
Filtrowanie zapytań¶
✅ ZAWSZE:
❌ NIGDY:
Decorators¶
@login_and_team_required
def client_list(request, team_slug):
"""Automatycznie sprawdza dostęp do team."""
team = request.team # Automatycznie dodane
clients = Client.objects.filter(team=team)
# ...
🏗️ Implementacja¶
Team Model¶
class Team(SubscriptionModelBase, BaseModel):
"""Team (virtual tenant) model."""
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
nip = models.CharField(max_length=10)
regon = models.CharField(max_length=14, blank=True)
# ...
Membership¶
class Membership(BaseModel):
"""Członkostwo użytkownika w zespole."""
team = models.ForeignKey(Team, on_delete=models.CASCADE)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
# ...
BaseTeamModel¶
class BaseTeamModel(BaseModel):
"""Abstract model for objects that are part of a team."""
team = models.ForeignKey("teams.Team", on_delete=models.CASCADE)
class Meta:
abstract = True
🔄 URL Structure¶
Team-based URLs¶
Wszystkie URL-e biznesowe są w kontekście zespołu:
Przykłady:
- /a/my-team/clients/ - Lista klientów
- /a/my-team/clients/123/ - Szczegóły klienta
- /a/my-team/invoicing/ - Faktury
URL Patterns¶
# urls.py
app_name = 'crm'
team_urlpatterns = [
path('', views.client_list, name='client_list'),
path('<int:pk>/', views.client_detail, name='client_detail'),
]
🛡️ Best Practices¶
1. Zawsze filtruj po team¶
# ✅ DOBRZE
clients = Client.objects.filter(team=request.team)
# ❌ ŹLE
clients = Client.objects.all()
2. Używaj BaseTeamModel¶
# ✅ DOBRZE
class Client(BaseTeamModel):
name = models.CharField(max_length=200)
# ❌ ŹLE
class Client(models.Model):
team = models.ForeignKey(Team, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
3. Używaj decorators¶
# ✅ DOBRZE
@login_and_team_required
def client_list(request, team_slug):
# ...
# ❌ ŹLE
def client_list(request, team_slug):
# Brak sprawdzania dostępu!
4. Optymalizuj zapytania¶
# ✅ DOBRZE - select_related dla FK
clients = Client.objects.filter(team=team).select_related('created_by')
# ✅ DOBRZE - prefetch_related dla M2M
clients = Client.objects.filter(team=team).prefetch_related('contacts')
📊 Database Indexes¶
Wymagane indeksy¶
Wszystkie tabele z team_id powinny mieć indeks:
class Meta:
indexes = [
models.Index(fields=['team', '-created_at']),
models.Index(fields=['team', 'name']),
]
🧪 Testowanie¶
Testy izolacji¶
def test_team_isolation(client):
"""Test czy dane są izolowane między teamami."""
team1 = Team.objects.create(name="Team 1", slug="team1")
team2 = Team.objects.create(name="Team 2", slug="team2")
client1 = Client.objects.create(team=team1, name="Client 1")
client2 = Client.objects.create(team=team2, name="Client 2")
# Team 1 nie powinien widzieć klientów Team 2
clients_team1 = Client.objects.filter(team=team1)
assert client1 in clients_team1
assert client2 not in clients_team1
📚 Więcej informacji¶
- Przegląd - Ogólny przegląd architektury
- Bazy Danych - Struktura bazy danych
- API - Architektura API
Ostatnia aktualizacja: 2025-11-29
Wersja dokumentacji: 1.0