如何使用EF Core将Point结构体数组高效映射到SQL Server数据库
如何使用EF Core将Point结构体数组高效映射到SQL Server数据库
我懂你的痛点!几千个Point要存到SQL Server,EF Core 8默认把原始类型数组(比如double[])转成JSON字符串存在nvarchar(max)里,空间开销大不说,序列化反序列化也慢——毕竟你根本不需要在数据库里搜索单个点,纯二进制存储才是最适合的方案,既省空间又快。
先给你捋清楚EF Core 8的默认行为:它会把所有原始类型数组自动序列化成JSON存在字符串列里,这就是你看到double[]映射成nvarchar(max)的原因,但显然这个方式对你的场景不友好。下面给你两个实用的二进制存储方案,按需选就行。
方案一:拆分Point数组为X/Y双精度数组,分别存为二进制
这个方案是把Point[]拆成两个double[](X坐标数组和Y坐标数组),然后用值转换器把每个数组序列化成二进制,存在SQL Server的varbinary列里。
1. 定义实体和Point结构体
首先是你的Point结构体,还有包含坐标数组的实体——我还加了一个非映射的Point[]属性,方便业务层直接使用,不用手动拆分数组:
public struct Point { public double X { get; set; } public double Y { get; set; } } public class MapEntity { public int Id { get; set; } // 映射到数据库的X/Y坐标数组 public double[] XCoordinates { get; set; } public double[] YCoordinates { get; set; } // 业务层用的Point数组,不映射到数据库 [NotMapped] public Point[] Points { get => XCoordinates.Zip(YCoordinates, (x, y) => new Point { X = x, Y = y }).ToArray(); set { XCoordinates = value.Select(p => p.X).ToArray(); YCoordinates = value.Select(p => p.Y).ToArray(); } } }
2. 配置值转换器和列类型
在DbContext的OnModelCreating方法里,为X/Y数组配置值转换器,把double[]转成byte[](二进制),同时指定列类型为varbinary(max):
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 为XCoordinates配置二进制序列化 modelBuilder.Entity<MapEntity>() .Property(e => e.XCoordinates) .HasConversion( // 把double[]序列化成byte[] array => { if (array == null) return null; using var ms = new MemoryStream(); using var writer = new BinaryWriter(ms); writer.Write(array.Length); // 先写数组长度,反序列化时用 foreach (var d in array) { writer.Write(d); } return ms.ToArray(); }, // 把byte[]反序列化成double[] bytes => { if (bytes == null) return null; using var ms = new MemoryStream(bytes); using var reader = new BinaryReader(ms); var length = reader.ReadInt32(); var array = new double[length]; for (int i = 0; i < length; i++) { array[i] = reader.ReadDouble(); } return array; }) .HasColumnType("varbinary(max)"); // 同样配置YCoordinates modelBuilder.Entity<MapEntity>() .Property(e => e.YCoordinates) .HasConversion( array => { if (array == null) return null; using var ms = new MemoryStream(); using var writer = new BinaryWriter(ms); writer.Write(array.Length); foreach (var d in array) { writer.Write(d); } return ms.ToArray(); }, bytes => { if (bytes == null) return null; using var ms = new MemoryStream(bytes); using var reader = new BinaryReader(ms); var length = reader.ReadInt32(); var array = new double[length]; for (int i = 0; i < length; i++) { array[i] = reader.ReadDouble(); } return array; }) .HasColumnType("varbinary(max)"); }
方案二:直接序列化整个Point数组为二进制
如果你不想拆分数组,也可以直接把整个Point[]序列化成二进制,存在单个varbinary列里,业务层使用更直接:
1. 简化实体定义
public struct Point { public double X { get; set; } public double Y { get; set; } } public class MapEntity { public int Id { get; set; } public Point[] Points { get; set; } }
2. 配置Point[]的二进制值转换器
同样在OnModelCreating里配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<MapEntity>() .Property(e => e.Points) .HasConversion( // 把Point[]序列化成byte[] points => { if (points == null) return null; using var ms = new MemoryStream(); using var writer = new BinaryWriter(ms); writer.Write(points.Length); // 先写数组长度 foreach (var p in points) { writer.Write(p.X); writer.Write(p.Y); } return ms.ToArray(); }, // 把byte[]反序列化成Point[] bytes => { if (bytes == null) return null; using var ms = new MemoryStream(bytes); using var reader = new BinaryReader(ms); var length = reader.ReadInt32(); var points = new Point[length]; for (int i = 0; i < length; i++) { points[i] = new Point { X = reader.ReadDouble(), Y = reader.ReadDouble() }; } return points; }) .HasColumnType("varbinary(max)"); }
注意事项
- 二进制存储的内容在数据库里是不可读的,但你不需要搜索单个点,完全不影响使用;
- 要保证序列化和反序列化的逻辑一致,比如不要随便修改Point结构体的字段顺序,不然反序列化会出错;
- 配置完后记得生成EF Core迁移,更新数据库结构,让列类型变成varbinary(max);
- 这两个方案的空间开销极小:每个double占8字节,一个Point就是16字节,加上4字节的数组长度,几千个点的话总大小也就几十KB,比JSON存储省太多。
内容来源于stack exchange
相关产品推荐
相关产品推荐

