簡體   English   中英

MongoEngine 中的經典映射

[英]Classical mapping in MongoEngine

我是 MongoEngine 的新手,看起來我們需要從mongoengine創建類Document的子類來建模我們的數據庫。 我在這里有點擔心,因為這違反了 SOLID 原則中的依賴倒置。 因此,如果我以后需要使用另一個數據庫,我將不得不更改我不應該做的領域模型類。

SQLAlchemy 通過提供漂亮的經典映射克服了這個問題。 使用它,依賴於數據庫的代碼與我的域模型分離,所以我真的不需要擔心數據庫提供者,如果我需要更改我的數據庫,我可以輕松地抽象出細節。

MongoDB是否有類似的東西,最好是在MongoEngine中?

Pymongo 的官方文檔提供了現有 ORM/ODM 和框架的列表,但據我所知,它們都實現了Active Record 模式(就像 django ORM 一樣),正如你所說,這違反了 SOLID 原則,但對於許多簡單的用例來說已經足夠了.

受 SQLAlchemy 啟發的 MongoAlchemy 使用會話的概念,因此它可能更接近您正在尋找的內容,但不再維護該項目。

如果我理解正確,您正在嘗試使用 mongoengine 將對象映射到文檔模式。

讓我們為用戶創建一個文檔類:

from mongoengine import Document, StringField


class UserDocument(Document):
    username = StringField(required=True)
    password = StringField(required=True)
    email = StringField(required=True)

現在添加一個創建新用戶的類方法:

from mongoengine import disconnect, connect, Document, StringField


class UserDocument(Document):
    username = StringField(required=True)
    password = StringField(required=True)
    email = StringField(required=True)

    @classmethod
    def new(cls):
        data = UserDocument(username=cls.username, password=cls.password, email=cls.email)
        connect('test_collection')
        data.save()
        disconnect('test_collection')

據我了解您的問題,您在此示例中的問題是 UserDocument 會意識到 mongoengine 因此違反了依賴倒置原則。 這可以通過子類來解決。

首先允許在 UserDocument 中繼承:

...
class UserDocument(Document):
    meta = {'allow_inheritance': True}
    username = StringField(required=True)
    ...

接下來我們構建孩子:

from user_document import UserDocument


# Maps object to schema
class User(UserDocument):
    def __init__(self, *args, **values):
        super().__init__(*args, **values)

接下來添加一個創建方法:

from user_document import UserDocument


# Maps object to schema
class User(UserDocument):
    def __init__(self, *args, **values):
        super().__init__(*args, **values)

    def create(self, username, password, email):
        self.username, self.password, self.email = username, password, email
        User.new()

現在我們的 User 對象繼承了 UserDocument 字段。 可以直接訪問 UserDocument.new,也可以使用 User.new() 通過子級訪問。

from model import User

    username, password, email = 'cool username', 'super secret password', 'mrcool@example.com'
    User.create(User, username, password, email)

User 對象知道 UserDocument,而后者又依賴於 mongoengine。

如果我誤解或使用了不正確的詞匯來描述示例解決方案,我深表歉意。 我相對較新,自學成才,沒有朋友編寫代碼,這使得討論變得困難。

這個主題在CosmicPython/Architecture Patterns With Python的前 6 章中有介紹。 但是,在那些章節中,它使用 SQLAlchemy 和映射器。

這本書確實有一個部分,其中包含使用 ActiveRecord 樣式的其他 ORM 的示例——如 mongoengine——在附錄 D:帶有 Django 的存儲庫和工作單元模式

首先定義模型。

請注意,如果沒有任何背景,下面的例子可能很難理解,所以如果下面的例子不清楚,我建議閱讀 CosmicPython 的前 6 章。

src/djangoproject/alloc/models.py

from django.db import models
from allocation.domain import model as domain_model


class Batch(models.Model):
    reference = models.CharField(max_length=255)
    sku = models.CharField(max_length=255)
    qty = models.IntegerField()
    eta = models.DateField(blank=True, null=True)

    @staticmethod
    def update_from_domain(batch: domain_model.Batch):
        try:
            b = Batch.objects.get(reference=batch.reference)
        except Batch.DoesNotExist:
            b = Batch(reference=batch.reference)
        b.sku = batch.sku
        b.qty = batch._purchased_quantity
        b.eta = batch.eta
        b.save()
        b.allocation_set.set(
            Allocation.from_domain(l, b)
            for l in batch._allocations
        )

    def to_domain(self) -> domain_model.Batch:
        b = domain_model.Batch(
            ref=self.reference, sku=self.sku, qty=self.qty, eta=self.eta
        )
        b._allocations = set(
            a.line.to_domain()
            for a in self.allocation_set.all()
        )
        return b


class OrderLine(models.Model):
    orderid = models.CharField(max_length=255)
    sku = models.CharField(max_length=255)
    qty = models.IntegerField()

    def to_domain(self):
        return domain_model.OrderLine(
            orderid=self.orderid, sku=self.sku, qty=self.qty
        )

    @staticmethod
    def from_domain(line):
        l, _ = OrderLine.objects.get_or_create(
            orderid=line.orderid, sku=line.sku, qty=line.qty
        )
        return l


class Allocation(models.Model):
    batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
    line = models.ForeignKey(OrderLine, on_delete=models.CASCADE)

    @staticmethod
    def from_domain(domain_line, django_batch):
        a, _ = Allocation.objects.get_or_create(
            line=OrderLine.from_domain(domain_line),
            batch=django_batch,
        )
        return a

然后在src/allocation/adapters/repository.py中為存儲庫模式定義一個端口和適配器

# pylint: disable=no-member, no-self-use
from typing import Set
import abc
from allocation.domain import model
from djangoproject.alloc import models as django_models


class AbstractRepository(abc.ABC):
    def __init__(self):
        self.seen = set()  # type: Set[model.Batch]

    def add(self, batch: model.Batch):
        self.seen.add(batch)

    def get(self, reference) -> model.Batch:
        p = self._get(reference)
        if p:
            self.seen.add(p)
        return p

    @abc.abstractmethod
    def _get(self, reference):
        raise NotImplementedError


class DjangoRepository(AbstractRepository):
    def add(self, batch):
        super().add(batch)
        self.update(batch)

    def update(self, batch):
        django_models.Batch.update_from_domain(batch)

    def _get(self, reference):
        return (
            django_models.Batch.objects.filter(reference=reference)
            .first()
            .to_domain()
        )

    def list(self):
        return [b.to_domain() for b in django_models.Batch.objects.all()]

連同域模型src/allocation/domain/model.py

from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from typing import Optional, List, Set


class OutOfStock(Exception):
    pass


def allocate(line: OrderLine, batches: List[Batch]) -> str:
    try:
        batch = next(b for b in sorted(batches) if b.can_allocate(line))
        batch.allocate(line)
        return batch.reference
    except StopIteration:
        raise OutOfStock(f"Out of stock for sku {line.sku}")


@dataclass(unsafe_hash=True)
class OrderLine:
    orderid: str
    sku: str
    qty: int


class Batch:
    def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
        self.reference = ref
        self.sku = sku
        self.eta = eta
        self._purchased_quantity = qty
        self._allocations = set()  # type: Set[OrderLine]

    def __repr__(self):
        return f"<Batch {self.reference}>"

    def __eq__(self, other):
        if not isinstance(other, Batch):
            return False
        return other.reference == self.reference

    def __hash__(self):
        return hash(self.reference)

    def __gt__(self, other):
        if self.eta is None:
            return False
        if other.eta is None:
            return True
        return self.eta > other.eta

    def allocate(self, line: OrderLine):
        if self.can_allocate(line):
            self._allocations.add(line)

    def deallocate(self, line: OrderLine):
        if line in self._allocations:
            self._allocations.remove(line)

    @property
    def allocated_quantity(self) -> int:
        return sum(line.qty for line in self._allocations)

    @property
    def available_quantity(self) -> int:
        return self._purchased_quantity - self.allocated_quantity

    def can_allocate(self, line: OrderLine) -> bool:
        return self.sku == line.sku and self.available_quantity >= line.qty

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM