Entity - ScutGame/Scut GitHub Wiki
此章节介绍如何设计服务端的实体Entity结构
实体是游戏中基本的存储单元,它可以通过Protobuf序列化的方式存储在Redis库中;还可以通过实体定义的Schema信息生成数据库的表结构,并且保存到数据库中。
使用如下定制属性提供实体Schema架构信息,如下属性:
- EntityTable:提供数据库连接串,表名,索引,以及存储方式和Cache结构类型等信息,只能放在类定义上面,EntityTable只能存在一个;
- EntityField:提供数据库表的字段,主键,字段类型,长度,唯一键和定义对象序列化方式,只能放在成员属性上面,EntityField只能存在一个;
- EntityFieldExtend:定义实体的字段是可以被子类继承的,只能放在成员属性上面,EntityFieldExtend只能存在一个;
序列化标记属性
- ProtoContract:Protobuf提供的序列化类配置;
- ProtoMember:Protobuf提供的序列化成员属性配置,需要提供Tag标签参数,Tag在类与子类中的成员都必须唯一,不能重复,否则序列化会失败;
- JsonIgnore: Json提供的序列化成员配置,有此标记的成员将不被序列化,其它的成员都可序列化,与Protobuf不同;
-
connectKey:数据库连接串配置键值,读取Config配置文件中connectionStrings节点的name值;
-
tableName:映射数据库中的表名, 为空时取类名作为表名;
-
periodTime:配置数据加载到缓存中过期时间多久,0表示不过期,单位秒;
-
personalName:指定玩家ID在数据库表中定义的字段名,默认是UserId,不区别大小写,配合定义BaseEntity类型的实体使用;
-
accessLevel:指定实体的访问级别,设置为只读时缓存修改不会自动更新到数据库或Redis中,ReadOnly是给策划配置类型的数值;
-
cacheType:指定使用的缓存结构类型,分为单个实体方式存储,字典方式存储(使用GetIdentity方法作为字典的Key),队列方式存储(在聊天模型中使用);
-
isStoreInDb:这个在6.7.9.6以上已经不在使用;
使用"成员属性名="设置值
-
IncreaseStartNo:设置自增方式的主键的起始编号,此编号不同于数据库中的自增编号;
-
IsExpired:设置实体是否会过期的,配合periodTime使用;
-
StorageType:设置缓存的数据从Redis或数据库加载方式,以及是否可以同步到Redis或数据库,使用 '|'或运算符号可以配置多个;
-
Capacity:指定从数据库加载数据的数量,同数据库中的Top语句;
-
Condition:指定从数据库加载数据的过滤条件,同数据库中的where语句;
-
OrderColumn:指定从数据库加载数据的排序方式,同数据库中的orderby语句;
-
TableNameFormat:自定表生成规则,在游戏中针对游戏日志以年月方式归档存储使用;
-
Indexs: 提供创建数据库表的索引配置,允许创建多组索引,其中字符串可以使用','来设置复合索引字段;
-
示例:
(1) 创建一个UserId的索引和以多列"Col1,Col2"组合的索引;
[EntityTable(AccessLevel.WriteOnly, DbCofingKeys.LogKey, Indexs=new[] { "UserID", "Col1,Col2"})]
public class MyEntityLog : LogEntity
{
[EntityField(true, IsIdentity = true)]
public long LogId { get; set; }
[EntityField]
public int UserID { get; set; }
[EntityField]
public int Col1 { get; set; }
[EntityField]
public int Col2 { get; set; }
}
(2) 创建日志表,"Log_"是固定字符,“$date[yyyyMMdd]"是日期表达式,"{0}"是Model的名称,如果一年中的第几周表示"$week",如:"Log_$date[yyyyMM]$week_{0}";
[EntityTable(AccessLevel.WriteOnly, DbCofingKeys.LogKey, TableNameFormat="Log_$date[yyyyMMdd]_{0}")]
public class MyEntityLog : LogEntity
{
}
(3) 从数据库查找UserId为1380000,取前100条,并以ID降序
[EntityTable(AccessLevel.ReadWrite, DbCofingKeys.DataKey, Capacity=100, Condition="UserId=1380000", OrderColumn="ID DESC", StorageType=StorageType.ReadWriteDB ")]
public class MyEntityData : BaseEntity
{
[EntityField(true)]
public int ID{ get; set;}
[EntityField(true)]
public int UserId{ get; set;}
}
-
fieldName:设置数据库表中的字段名,为空时取属性的名称;
-
isKey:设置数据库表中的主键,当多个属性都设置为True时,表示主键是复合主键;
-
isJsonSerialize:设置属性是Ojbect类型的字段且可Json序列化的;
-
dbType:设置属性在数据库表中的字段类型,如果未设置则根据属性的类型自动匹配;
使用"成员属性名="设置值
-
ColumnLength:设置数据库表中字段的长度,varchar默认值为255;
-
ColumnScale:设置数据库表中字段为符点数时保留的小数位数;
-
Isnullable:设置数据库表中字段是否可为空值;
-
IsUnique:设置数据库表中字段的唯一约束,多个属性都配置时表示复合唯一约束,暂不支持多组唯一约束配置;
-
IsIdentity:设置数据库表中字段是可自增的字段, 设置为true后,此对象将不能Update的方式同步到数据了,可以Insert和Delete;
-
IdentityNo:设置数据库表中可自增的字段的起始值,与IsIdentity一起使用;
-
Disable:设置字段不从数据库中取值;
-
JsonDateTimeFormat:设置字段为Object类型时,Json序列化的日期格式,与
isJsonSerialize
配合使用;
设置类的属性可以在子类中使用,是可继承的。
- 示例:
定义Col1与Col2属性是可以被子类继承的;
[EntityTable(CacheType.Dictionary, DbCofingKeys.DataKey})]
public class MyEntity : BaseEntity
{
[EntityField]
public int UserID { get; set; }
[EntityField]
[EntityFieldExtend]
public int Col1 { get; set; }
[EntityField]
[EntityFieldExtend]
public int Col2 { get; set; }
}
在MyEntityExtend类中包括父亲的Col1与Col2属性;
[EntityTable(CacheType.Dictionary, DbCofingKeys.DataKey})]
public class MyEntityExtend : MyEntity
{
[EntityField]
public int UserID { get; set; }
[EntityField]
public int Col3 { get; set; }
}
结构包括:私有(BaseEntity)、共享(ShareEntity)、排名(RankEntity)、日志(LogEntity)类型;其中共享以分为只读和读写类型;共享只读类型主要是定义策划的数值配置。
此实体子类是定义属于玩家私有类型的数据,需要配置EntityTable的CacheType属性为Dictionary,与PersonalCacheStruct缓存一起使用,成员中必须包含以UserId为名称的主键字段;
[ProtoContract]
[EntityTable(CacheType.Dictionary, "{数据库连接配置键}"})]
public class MyEntity : BaseEntity
{
[ProtoMember(1)]
[EntityField(true)]
public int UserId { get; set; }
protected override int GetIdentityId()
{
return UserId;
}
}
此实体子类是定义属于玩家私有类型的数据,需要配置EntityTable的CacheType属性为Entity,与PersonalCacheStruct缓存一起使用,成员中必须包含一个主键属性;
[ProtoContract]
[EntityTable(CacheType.Entity, "{数据库连接配置键}"})]
public class EntityData : ShareEntity
{
[ProtoMember(1)]
[EntityField(true)]
public int ID { get; set; }
}
只读类型
需要配置EntityTable的AccessLevel为ReadOnly,此实体不能写,只从数据库中读取,可以不定义序列化标记,定义也不会有错;
[ProtoContract]
[EntityTable(AccessLevel.ReadOnly, "{数据库连接配置键}"})]
public class EntityInfo : ShareEntity
{
[ProtoMember(1)]
[EntityField(true)]
public int ID { get; set; }
}
此实体子类是针对有序类型的数据,需要配置EntityTable的CacheType属性为Rank,与RankCacheStruct缓存一起使用;
示例: 聊天功能,聊天数据以MsgType
分类在Redis中存储
public enum MsgType
{
/// <summary>
/// 1:世界
/// </summary>
Word=1,
/// <summary>
/// 2:联盟
/// </summary>
Union,
/// <summary>
/// 3:私聊
/// </summary>
Whisper,
/// <summary>
/// 4:其它
/// </summary>
Other
}
[ProtoContract]
[EntityTable(CacheType.Rank, "", StorageType = StorageType.ReadWriteRedis)]
public class ChatInfo : RankEntity
{
public override string Key
{
get { return MsgType.ToString(); }
}
[ProtoMember(1)]
public MsgType MsgType { get; set; }
[ProtoMember(2)]
public string Message { get; set; }
[ProtoMember(3)]
public DateTime SendTime { get; set; }
}
使用RankCacheStruct
操作RankEntity
实体,如:
var cache = new RankCacheStruct<ChatInfo>();
var chatInfo = new ChatInfo()
{
MsgType = MsgType.Word,
Message = "Hello",
Score = DateTime.Now.Ticks
};
cache.AddRank(chatData.Key, chatInfo);//根据Score字段插入排序
//取前50条记录
List<ChatInfo> items;
if (cache.TryTakeRank(msgType, 50, out items))
{
}
还有ExchangeRank
方法将名次1与2的两个对象对调。
日志类型的实体是不需要在cache中存储的,需要配制EntityTable的AccessLevel属性为只写的,直接使用DataSyncQueueManager类更新到数据库或redis库,成员中必须包含一个主键属性,可以设置数据库自增;
在数据库中表是以年月的方式存储,当数据增长过大需要清理数据库时,可以将旧月份的表直接删除再收缩数据库,默认预先创建3个月的表;
[EntityTable(AccessLevel.WriteOnly, "{数据库连接配置键}"})]
public class MyLog : LogEntity
{
[EntityField(true, IsIdentity = true, IdentityNo = 100)]
public long LogId { get; set; }
}
定义实体的成员属性可以包括:基础类型(byte、bool、short、int、long、float、double、decimal、string和枚举等),以及EntityChangeEvent子类对象,CacheList列表和CacheDictionary字典等类型;
基础类型的成员属性的定义就不详细介绍了,这里主要介绍下如何定义自定的对象类型成员;
它作为Entity成员属性中的自定定义类型,Json序列化存储在表的字段中,继承它的子类对象将具有修改通知事件,修改后的属性会同步到Redis或数据库中。
定义背包项BagItem子类
[ProtoContract]
public class BagItem : EntityChangeEvent
{
[ProtoMember(1)]
public int Id { get; set; }
[ProtoMember(2)]
public int Num { get; set; }
}
定义玩家UserBag背包实体,自定义成员属性需要配置isJsonSerialize为true,且数据库列的类型为Text(在mysql中text与longtext有区分,mssql不区分),
[ProtoContract]
[EntityTable(CacheType.Dictionary, "{数据库连接配置键}"})]
public class UserBag : BaseEntity
{
[ProtoMember(1)]
[EntityField(true)]
public int UserId { get; set; }
[ProtoMember(2)]
[EntityField(true, ColumnDbType.LongText)]
public BagItem Item { get; set; }
protected override int GetIdentityId()
{
return UserId;
}
}
同以上背包示例,修改成列表结构如下:
[ProtoContract]
[EntityTable(CacheType.Dictionary, "{数据库连接配置键}"})]
public class UserBag : BaseEntity
{
[ProtoMember(1)]
[EntityField(true)]
public int UserId { get; set; }
[ProtoMember(2)]
[EntityField(true, ColumnDbType.LongText)]
public CacheList<BagItem> Items { get; set; }
protected override int GetIdentityId()
{
return UserId;
}
}
同以上背包示例,修改成字典结构如下:
[ProtoContract]
[EntityTable(CacheType.Dictionary, "{数据库连接配置键}"})]
public class UserBag : BaseEntity
{
[ProtoMember(1)]
[EntityField(true)]
public int UserId { get; set; }
[ProtoMember(2)]
[EntityField(true, ColumnDbType.LongText)]
public CacheDictionary<int, BagItem> Items { get; set; }
protected override int GetIdentityId()
{
return UserId;
}
}
- 在Enitty属性被修改时,需要额外做些事情,那么开发者要如何处理呢?
开发者首先都会想到直接在Enitty的属性里修改,为了保持Entity的干净,尽量不增加的其它方法,可以在CsScript目录下增加一层实体操作层(Controller),负责对实体的增册改查操作;