Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dropping ACID - Schema Design for e-commerce

alcaeus
January 29, 2020

Dropping ACID - Schema Design for e-commerce

Schema design presentation given at Symfony User Group Berlin

alcaeus

January 29, 2020
Tweet

More Decks by alcaeus

Other Decks in Programming

Transcript

  1. Product Entity /** @final */ class Product { private ?string

    $id = null; private string $title; private ?string $description = null; /** @var Collection<ProductVariant> */ private Collection $variants; /** @var Collection<ProductOption> */ private Collection $options; public function __construct(string $title) { $this->title = $title; $this->variants = new ArrayCollection(); $this->options = new ArrayCollection(); } }
  2. Order Entity /** @final */ class Order { private ?string

    $id = null; private string $state = 'cart'; private ?Customer $customer = null; /** @var Collection<OrderItem> */ private Collection $items; private int $total = 0; public function __construct() { $this->items = new ArrayCollection(); } }
  3. Database Normalisation • Unnormalised: “Just do whatever” • 1NF: “Domain

    of each attribute contains only atomic values” • … • 6NF: “Row contains the Primary Key, and at most one other attribute”
  4. ERD -> Tables CREATE TABLE sylius_product_option (...); CREATE TABLE sylius_product_option_value

    (...); CREATE TABLE sylius_product (...); CREATE TABLE sylius_product_options (...); CREATE TABLE sylius_product_variant (...); CREATE TABLE sylius_product_variant_option_value (...); CREATE TABLE sylius_order (...); CREATE TABLE sylius_order_item (...); CREATE TABLE sylius_order_item_unit (...);
  5. Create a Product $product = new Product(); $product->setTitle('Symfony T-Shirt'); $product->addProductOption($color);

    $variant = new ProductVariant($product); $variant->setProductOptionValue($color, $black); $manager->persist($product); $manager->persist($variant); $manager->flush();
  6. Write to the Database INSERT INTO sylius_product VALUES (...); INSERT

    INTO sylius_product_options VALUES (...); INSERT INTO sylius_product_variant VALUES (...); INSERT INTO sylius_product_variant_option_value (...);
  7. Write to the Database INSERT INTO sylius_product VALUES (...); INSERT

    INTO sylius_product_options VALUES (...); INSERT INTO sylius_product_variant VALUES (...); INSERT INTO sylius_product_variant_option_value (...);
  8. Write to the Database INSERT INTO sylius_product VALUES (...); INSERT

    INTO sylius_product_options VALUES (...); INSERT INTO sylius_product_variant VALUES (...); INSERT INTO sylius_product_variant_option_value (...); START TRANSACTION; COMMIT;
  9. Finalise Payment START TRANSACTION; UPDATE sylius_payment SET state = 'completed'

    ...; UPDATE sylius_order SET payment_state = 'completed' ...; COMMIT;
  10. Cart { "id": 123, "customer": 45, "state": "cart", "items": [

    { "variant": 1381, "price": 1499, "quantity": 2, "units": [ { "id": "1381-1", "shipped": false }, { "id": "1381-2", "shipped": false } ] } ], "items_total": 2998 }
  11. Order { "id": 123, "customer": { "id": 45, "name": "Andreas

    Braun", "shippingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" }, "billingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" } }, "state": "new", "items": [ { "variant": 1381, "price": 1499, "quantity": 2, "units": [ { "id": "1381-1", "shipped": true }, { "id": "1381-2", "shipped": true } ] } ], "items_total": 2998, "payments": [ { "amount": 2998, "state": "completed", "payment_method": { "type": "credit_card", "details": { "cardHolder": "Andreas Braun", "creditCard": "xxxxxxxxxxxx1234", "expiration": "09/24", "gateway": "easy-payment-service", "identifier": "afae87a0" } }, "billingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" } } ], "shipments": [ { "units": ["1381-1", "1381-2"], "state": "registered", "shipment_method": { "type": "dhl", "trackingNumber": "12345678a" }, "shippingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" }, "history": [ { "date": "2019-11-21T14:23:04+01:00", "state": "registered" } ] } ] }
  12. Order - Customer { "customer": { "id": 45, "name": "Andreas

    Braun", "shippingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" }, "billingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" } } }
  13. Order - Payments { "payments": [ { "amount": 2998, "state":

    "completed", "payment_method": { "type": "credit_card", "details": { "cardHolder": "Andreas Braun", "creditCard": "xxxxxxxxxxxx1234", "expiration": "09/24", "gateway": "easy-payment-service", "identifier": "afae87a0" } }, "billingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" } } ] }
  14. Order - Shipments { "shipments": [ { "units": ["1381-1", "1381-2"],

    "state": "registered", "shipment_method": { "type": "dhl", "trackingNumber": "12345678a" }, "shippingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" }, "history": [ { "date": "2019-11-21T14:23:04+01:00", "state": "registered" } ] } ] }
  15. References • A pointer to another entity • Referential integrity

    optional (MyISAM, MongoDB) • State of the art in an RDBMS
  16. Embedded data • Not just a pointer to another entity,

    but all data • Update is done on the main entity • Not very useful if data is needed by itself
  17. Choose Your Relationship • Reference data when it’s needed by

    itself • Embed data when it’s owned by another entity • Look at your queries
  18. JSON/JSONb UPDATE sylius_order SET payments = '[ { "amount": 2998,

    "state": "completed", "payment_method": { "type": "credit_card", "details": { "cardHolder": "Andreas Braun", "creditCard": "xxxxxxxxxxxx1234", "expiration": "09/24", "gateway": "easy-payment-service", "identifier": "afae87a0" } }, "billingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" } } ]';
  19. Partial JSON Updates UPDATE sylius_order SET payments = JSON_SET( payments,

    '$[last].state', 'failed', ‘$[last].payment_method.details.error', '...' ) ;
  20. Partial JSON Updates UPDATE sylius_order SET payments = JSON_ARRAY_APPEND( JSON_SET(

    payments, '$[last].state', 'failed', ‘$[last].payment_method.details.error', '...' ), CAST('{ "amount": 2998, "state": "pending", ... }' AS JSON) ) ;
  21. Complex Types CREATE TYPE customer AS ( id INT, name

    VARCHAR(255), shippingAddress address, billingAddress address );
  22. Update Arrays UPDATE sylius_order SET (payments[0]).state = 'failed', payments =

    payments || ARRAY[CAST(ROW(123, ...) AS payment)] ;
  23. Create Cart db.order.insert({ _id: 123, customer: 45, state: "cart", items:

    [ { variant: 1381, price: 1499, quantity: 2, units: [ { id: "1381-1", shipped: true }, { id: "1381-2", shipped: true } ] } ], items_total: 2998 });
  24. Finish Checkout db.order.updateOne( { _id: 123}, { $set: { 'checkout_state':

    'completed', 'state': 'awaiting_payment', 'customer': { 'name': 'Andreas Braun', ... } }, $push: { payments: { ... }, shipments: { ... } } } );
  25. Atomic Updates db.order.updateOne( { _id: 123 }, { $set: {

    'payments.$[failedPayment].state': 'failed', 'payments.$[failedPayment].details': { ... }, }, $push: { 'payments': { ... } } }, { arrayFilters: [ { 'failedPayment.id': { $eq: 1234 } } ] } );
  26. Product in ODM /** * @ODM\Document * @final */ class

    Product { /** @ODM\Id */ private ?string $id = null; /** @ODM\Field(type="string") */ private string $title; /** @ODM\Field(type="string") */ private ?string $description = null; /** @ODM\ReferenceMany(targetDocument=ProductVariant::class) */ private Collection $variants; /** @ODM\ReferenceMany(targetDocument=ProductOption::class) */ private Collection $options; }
  27. Order in ODM /** * @ODM\Document * @final */ class

    Order { /** @ODM\Id */ private ?string $id = null; /** @ODM\Field(type="string") */ private string $state = 'cart'; /** @ODM\EmbedOne(targetDocument=Customer::class) */ private ?Customer $customer = null; /** @ODM\EmbedMany(targetDocument=OrderItem::class) */ private Collection $items; /** @ODM\Field(type=int, strategy="increment") */ private int $total = 0; }
  28. Embedded Documents /** @ODM\EmbeddedDocument */ final class OrderItem { /**

    @ODM\ReferenceOne(targetDocument=ProductVariant::class) */ private ProductVariant $variant; /** @ODM\Field(type="int") */ private int $price; /** @ODM\Field(type="int", strategy="increment") */ private int $quantity; /** @ODM\EmbedMany(targetDocument=OrderItemUnit::class) */ private Collection $units; }
  29. Finish Checkout db.order.updateOne( { _id: 123}, { $set: { 'checkout_state':

    'completed', 'state': 'awaiting_payment', 'customer': { 'name': 'Andreas Braun', ... } }, $push: { payments: { ... }, shipments: { ... } } } );
  30. Hybrid Relationships • Keeps a reference to original entity •

    Stores some (or all) data in the relationship as well • Can be used in PostgreSQL or MongoDB
  31. Using Complex Types CREATE TYPE customer AS ( id INT,

    name VARCHAR(255), shippingAddress address, billingAddress address );
  32. Using DBRef in MongoDB , "name": "Andreas Braun", "shippingAddress": {

    "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" }, "billingAddress": { "address": "<redacted>", "postCode": "<redacted>", "city": "<redacted>", "country": "Germany" } { "id": 123, "customer": { "$ref": "customer", "$id": 45 } }