skip to Main Content

I use the EAV (Entity-Attribute-Value) pattern in an inventory management system I’m developing.
It’s very comparable to a typical webshop framework like Magento.

Attributes have a data type, like numeric, varchar, text.
The attribute definitions are stored in one table like eav_attributes. The values are stored in separate tables specific for the data type.
Say that a numeric attribute is assigned to a product. The database table that holds the relation between the attribute and product and the value, is a table like eav_decimal_attribute_values.
Example table scheme:

attribute_id | product_id | attribute_value (decimal)

And the same principle for a varchar attribute:

attribute_id | product_id | attribute_value (varchar(255))

When loading a product from the database, I load the product -> attribute -> value relations of each of that tables.
So I query the eav_decimal_attribute_values table to get all assigned decimal attributes and their values for the product (or get an empty resultset or course).
And the same for the other attribute types (eav_varchar_attribute_values, eav_text_attribute_values, …).
So far this is actually the same as how e.g. Magento manages this.

Since I use a typed language, I chose to have the following design:

class AttributeDefinition
    - string attributeName
    - string attributeDescription
    - enum dataType // Like 'numeric', 'varchar', 'text', ...
    - ...
    - ...

abstract class ProductAttributeAssignment
    - AttributeDefinition attribute

class DecimalProductAttribute : ProductAttributeAssignment
    - double value
    - ...

class VarcharProductAttribute : ProductAttributeAssignment
    - string value
    - ...

class TextProductAttribute : ProductAttributeAssignment
    - string value
    - ...

Product
    - name
    - ...
    - attributes
        // Problematic part: storing different types together
        - A TextProductAttribute 
        - Another TextProductAttribute
        - A DecimalProductAttribute
        - A VarcharProductAttribute
        - ...

In code I want to preserve types. Like a numeric attribute’s value is stored as a double or decimal value in code. A varchar or text attribute is put in a string.
Of course I could just create one concrete class ProductAttributeAssignment, and store the value always as a string in memory, regardless of the data type the attribute represents. But imagine that I have to do calculations with numeric attributes, or search features like Charging cable length > 120, it would be useful to preserve types. Also when there would be features like developers could create plugins or hooks.

The problem is that I have some difficulties in coding the product aggregate. It would be logical to store a products’s attribute assignments just in a Product object. But since there’s a different class for each attribute type, I cannot just write a class like Product with a field List<…> attributes.

So, my question is not about storing things in the database/persistent layer, but about writing code for a business object that owns things that are actually the "same business thing" but technically different types in code.

2

Answers


  1. I didn’t pick up on any database libraries or frameworks or anything in your post, so I’m assuming you’re just doing this in pure C#. You could do this with a list containing generic classes, (inspired by this post), either by keeping a List<object>, or a common parent interface/class.

    public class Product {
      // ...
      public List<AttributeBase> Attributes { get; private set; } = new List<AttributeBase>();
      // ...
    }
    
    public abstract class AttributeBase {
      public AttributeDefinition Definition { get; private set; }
      public abstract object ObjectValue { get; }
    
      public AttributeBase(AttributeDefinition definition) {
        Definition = definition;
      }
    }
    
    public class Attribute<T> : AttributeBase {
      public object ObjectValue => Value;
      public T Value { get; set; }
      
      public Attribute(AttributeDefinition definition, T value) : base(definition) {
        Value = value;
      }
    }
    

    And then you could use it like…

    Product product = ...;
    
    // Adding an attribute...
    AttributeDefinition definition = ...;
    Attribute<double> newAttribute = new(definition, 1.23);
    product.Attributes.Add(newAttribute);
    
    // Getting an attribute...
    AttributeBase attribute = product.Attributes[0];
    
    // You can get the value like this...
    object valueAsObject = attribute.ObjectValue;
    if (valueAsObject is double castedDouble) {
      Console.WriteLine(castedDouble + 0.654);
    }
    
    // OR like this...
    if (attribute is Attribute<double> doubleAttribute) {
      double value = doubleAttribute.Value;
      Console.WriteLine(value + 0.654);
    }
    
    // Example, Finding the attribute named "ProductSKU"
    AttributeBase skuAttribute = product.Attributes.FirstOrDefault(attrib => attrib.Definition.name == "ProductSKU");
    
    // Since WriteLine can take an object, it doesn't matter.
    Console.WriteLine(skuAttribute.ObjectValue);
    

    Or, if you wanted to avoid generics, you could just make child classes for each type, which allows for additional parameters and validators like a varchar’s size. (you could even mix both…):

    public class VarCharAttribute : AttributeBase {
      public object ObjectValue => Value;
      public string Value { 
        get => _value; 
        set {
          if (value.Length >= MaxLength) {
            value = value.Substring(0,MaxLength);
          }
          _value = value;
        }
      }
      public int MaxLength { get; private set; }
    
      private string _value;
    
      public VarCharAttribute(AttributeDefinition definition, string value, int limit) : base(definition) {
        MaxLength = limit;
        Value = value;
      }
    }
    

    But even something like this could have been done with the generic attribute using a class. There’s tons of ways to do this, my solution is just one of them; you have to pick whichever one would work best for your particular use-case.

    Login or Signup to reply.
  2. I think what @ipodtouch0218 suggested..
    You can use a generic collection -> AttributeDefinition,
    Based on the type double/string/float it will save the value and type would be retained for you.
    Keeping it generic would be fine considering EAV pattern.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search