【问题标题】:Storing decimal data type in Azure Tables在 Azure 表中存储十进制数据类型
【发布时间】:2018-05-12 21:42:19
【问题描述】:

Windows Azure 表存储does not support decimal 数据类型。

suggested workaround 是使用自定义属性将小数属性序列化为字符串:

[EntityDataType(PrimitiveTypeKind.String)]
public decimal Quantity { get; set; }

如何实现这个 EntityDataType 自定义属性,以便可以从 Windows Azure 表中存储和检索十进制属性?

【问题讨论】:

    标签: c# azure azure-table-storage


    【解决方案1】:

    在基类中覆盖ReadEntityWriteEntity 有利于这一点。不必每次检索实体时都写EntityResolver

    public class CustomTableEntity : TableEntity
    {
        public override void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext)
        {
            base.ReadEntity(properties, operationContext);
    
            foreach (var thisProperty in
                GetType().GetProperties().Where(thisProperty =>
                    thisProperty.GetType() != typeof(string) &&
                    properties.ContainsKey(thisProperty.Name) &&
                    properties[thisProperty.Name].PropertyType == EdmType.String))
            {
                var parse = thisProperty.PropertyType.GetMethods().SingleOrDefault(m =>
                    m.Name == "Parse" &&
                    m.GetParameters().Length == 1 &&
                    m.GetParameters()[0].ParameterType == typeof(string));
    
                var value = parse != null ?
                    parse.Invoke(thisProperty, new object[] { properties[thisProperty.Name].StringValue }) :
                    Convert.ChangeType(properties[thisProperty.Name].PropertyAsObject, thisProperty.PropertyType);
    
                thisProperty.SetValue(this, value);
            }
        }
    
        public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
        {
            var properties = base.WriteEntity(operationContext);
    
            foreach (var thisProperty in
                GetType().GetProperties().Where(thisProperty =>
                    !properties.ContainsKey(thisProperty.Name) &&
                    typeof(TableEntity).GetProperties().All(p => p.Name != thisProperty.Name)))
            {
                var value = thisProperty.GetValue(this);
                if (value != null)
                {
                    properties.Add(thisProperty.Name, new EntityProperty(value.ToString()));
                }
            }
    
            return properties;
        }
    }
    

    使用时,只需让你的实体从CustomTableEntity扩展,插入或检索实体时它将是透明的。支持DateTimeTimeSpandecimal以及那些有Parse方法或实现IConvertible接口的类型。

    【讨论】:

    • 清晰简单的解决方案,我喜欢。我要补充一件事。为此,您必须注意用于检索的方法。 TableOperation tabOp = TableOperation.Retrieve&lt;TEntity&gt;(partitionKey, key); 有效,简单的 Retrieve (..) 也有效,但如果您尝试强制转换 result.Result as TEntity,您会得到一个空变量。
    • 感谢您提供此解决方案!我想补充一点,这个WriteEntity 实现将写出标有[NotMapped] 属性的属性,这是不可取的。您可以为此添加一个检查并忽略 [NotMapped] 属性,方法是在 foreach 条件中添加类似这样的内容:!Attribute.IsDefined(thisProperty, typeof(NotMappedAttribute))
    • @ruttopia 最好使用[IgnoreProperty] 直接来自Microsoft.WindowsAzure.Storage.dll 所以!Attribute.IsDefined(thisProperty, typeof(IgnorePropertyAttribute))
    • 请注意,如果是十进制,您可以收到FormatException。但它会被包裹到TargetInvocationException 中。确保您使用相同的CultureInfo 来存储和检索数据。您也可以添加 if 语句以不同方式处理小数分隔符。
    【解决方案2】:

    您可以覆盖 TableEntity 中的 WriteEntity 方法并使用 EntityResolver

    public class CustomTableEntity : TableEntity
    {
        private const string DecimalPrefix = "D_";
    
        public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
        {
            var entityProperties = base.WriteEntity(operationContext);
            var objectProperties = GetType().GetProperties();
    
            foreach (var item in objectProperties.Where(f => f.PropertyType == typeof (decimal)))
            {
                entityProperties.Add(DecimalPrefix + item.Name, new EntityProperty(item.GetValue(this, null).ToString()));
            }
    
            return entityProperties;
        }
    }
    

    我们将使用的实体

    public class MyEntity : CustomTableEntity
    {
        public string MyProperty { get; set; }
    
        public decimal MyDecimalProperty1 { get; set; }
        public decimal MyDecimalProperty2 { get; set; }
    }
    

    用法,包括创建表格/插入/检索

    #region connection
    
    CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
    CloudTableClient client = account.CreateCloudTableClient();
    CloudTable table = client.GetTableReference("mytable");
    table.CreateIfNotExists();
    
    #endregion
    
    
    const string decimalPrefix = "D_";
    
    const string partitionKey = "BlaBlaBla";
    string rowKey = DateTime.Now.ToString("yyyyMMddHHmmss");
    
    
    #region Insert
    
    var entity = new MyEntity
        {
            PartitionKey = partitionKey,
            RowKey = rowKey,
            MyProperty = "Test",
            MyDecimalProperty1 = (decimal) 1.2,
            MyDecimalProperty2 = (decimal) 3.45
        };
    
    TableOperation insertOperation = TableOperation.Insert(entity);
    table.Execute(insertOperation);
    
    #endregion
    
    
    
    #region Retrieve
    
    EntityResolver<MyEntity> myEntityResolver = (pk, rk, ts, props, etag) =>
        {
            var resolvedEntity = new MyEntity {PartitionKey = pk, RowKey = rk, Timestamp = ts, ETag = etag};
    
            foreach (var item in props.Where(p => p.Key.StartsWith(decimalPrefix)))
            {
                string realPropertyName = item.Key.Substring(decimalPrefix.Length);
                System.Reflection.PropertyInfo propertyInfo = resolvedEntity.GetType().GetProperty(realPropertyName);
                propertyInfo.SetValue(resolvedEntity, Convert.ChangeType(item.Value.StringValue, propertyInfo.PropertyType), null);
    
            }
    
            resolvedEntity.ReadEntity(props, null);
    
            return resolvedEntity;
        };
    
    TableOperation retrieveOperation = TableOperation.Retrieve(partitionKey, rowKey, myEntityResolver);
    TableResult retrievedResult = table.Execute(retrieveOperation);
    var myRetrievedEntity = retrievedResult.Result as MyEntity;
    
    // myRetrievedEntity.Dump(); 
    
    #endregion
    

    【讨论】:

      【解决方案3】:

      您是否尝试过使用Lokad.Cloud FatEntities 产品?

      我认为他们只是对要存储在表中的整个对象使用二进制序列化程序。看看“Object-to-Cloud mapper”项目可能也是值得的:

      https://github.com/Lokad/lokad-cloud

      【讨论】:

      • 谢谢。这是一个有价值的建议,但它并没有解决关于如何实现所引用文章中描述的 EntityDataType 类的原始问题。
      【解决方案4】:

      @EUYUIL 提出了一个很好的通用解决方案,我使用它效果很好,但是按照他的回答,使用 Nullable 类型时它会失败。

             // Get the underlying types 'Parse' method
             if (curType.IsGenericType && curType.GetGenericTypeDefinition() == typeof(Nullable<>))
             {
                  curType = Nullable.GetUnderlyingType(curType);
             }
      

      如果它对任何人有帮助,请在 foreach 中覆盖 ReadEntity 方法的内容。可能有更好的方法来写这个,但为了说明目的,这样做就可以了。

              var curType = thisProperty.PropertyType;
      
              // Get the underlying types 'Parse' method
              if (curType.IsGenericType && curType.GetGenericTypeDefinition() == typeof(Nullable<>))
              {
                  curType = Nullable.GetUnderlyingType(curType);
              }
      
              var parse = curType.GetMethods().SingleOrDefault(m =>
                  m.Name == "Parse" &&
                  m.GetParameters().Length == 1 &&
                  m.GetParameters()[0].ParameterType == typeof(string));
      
              var value = parse != null ?
                  parse.Invoke(thisProperty, new object[] { properties[thisProperty.Name].StringValue }) :
                        Convert.ChangeType(properties[thisProperty.Name].PropertyAsObject, thisProperty.PropertyType);
      
      
              thisProperty.SetValue(this, value);
      

      【讨论】:

        【解决方案5】:

        您可以将属性类型更改为double。然后,您必须通过将表实体映射到您自己的域类型来在 decimaldouble 之间进行转换。另一种选择是在由单个 decimal 字段支持的实体上具有两个属性。但是,您可能希望继续为decimal 属性使用Quantity 名称,因为它是存储在表中的double 属性,您必须通过覆盖@987654330 将此属性重命名为Quantity @ 和 WriteEntity。那么你不妨使用这里提出的其他一些解决方案。

        现在,您可能认为将 decimal 存储为 double 会导致某些值无法正确往返。虽然肯定有一些值不会因为两种类型的范围和精度非常不同而往返,但大多数“正常”值(例如不是天文数字大且具有人类可读精度的货币值)将毫无问题地往返。这是因为Convert.ToDouble 执行的从doubledecimal 的转换有一个特殊的属性:

        此方法返回的 Decimal 值最多包含 15 个有效数字。

        以下是一个示例,说明如何将其他有问题的数字进行往返:

        var originalValue = 2.24M;
        var doubleValue = (double) originalValue;
        

        问题在于没有使用浮点数精确表示十进制数 2.24,就像没有使用十进制数精确表示有理数 1/3(2.24 是有理数 224/100)一样。 0.3333333333333333 不等于 1/3。您可以通过以足够的精度打印doubleValue 来验证这一点。 Console.WriteLine($"{doubleValue:G17}") 产量

        2.2400000000000002
        

        但是,往返值仍然有效:

        var roundTripValue = (decimal) doubleValue;
        

        现在Console.WriteLine(roundTripValue) 产生

        2.24
        

        只要您不对double 值进行任何计算,您就可以使用它们来存储decimal 值,前提是doubledecimal 之间的转换符合上面引用的.NET 规则.

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-05-31
          • 1970-01-01
          • 2011-12-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多