Entity - ScutGame/Scut GitHub Wiki

此章节介绍如何设计服务端的实体Entity结构

目的与用途

实体是游戏中基本的存储单元,它可以通过Protobuf序列化的方式存储在Redis库中;还可以通过实体定义的Schema信息生成数据库的表结构,并且保存到数据库中。

实体Schema架构信息

使用如下定制属性提供实体Schema架构信息,如下属性:

  • EntityTable:提供数据库连接串,表名,索引,以及存储方式和Cache结构类型等信息,只能放在类定义上面,EntityTable只能存在一个;
  • EntityField:提供数据库表的字段,主键,字段类型,长度,唯一键和定义对象序列化方式,只能放在成员属性上面,EntityField只能存在一个;
  • EntityFieldExtend:定义实体的字段是可以被子类继承的,只能放在成员属性上面,EntityFieldExtend只能存在一个;

序列化标记属性

  • ProtoContract:Protobuf提供的序列化类配置;
  • ProtoMember:Protobuf提供的序列化成员属性配置,需要提供Tag标签参数,Tag在类与子类中的成员都必须唯一,不能重复,否则序列化会失败;
  • JsonIgnore: Json提供的序列化成员配置,有此标记的成员将不被序列化,其它的成员都可序列化,与Protobuf不同;

EntityTable属性

构造函数参数:

  • 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;}

}

EntityField属性

构造函数参数:

  • 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配合使用;

EntityFieldExtend属性

设置类的属性可以在子类中使用,是可继承的。

  • 示例

定义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)类型;其中共享以分为只读和读写类型;共享只读类型主要是定义策划的数值配置。

私有(BaseEntity)

此实体子类是定义属于玩家私有类型的数据,需要配置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;
    }
}

共享(ShareEntity)

此实体子类是定义属于玩家私有类型的数据,需要配置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; }
}

排名(RankEntity)

此实体子类是针对有序类型的数据,需要配置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的两个对象对调。

日志(LogEntity)

日志类型的实体是不需要在cache中存储的,需要配制EntityTable的AccessLevel属性为只写的,直接使用DataSyncQueueManager类更新到数据库或redis库,成员中必须包含一个主键属性,可以设置数据库自增;

在数据库中表是以年月的方式存储,当数据增长过大需要清理数据库时,可以将旧月份的表直接删除再收缩数据库,默认预先创建3个月的表;

[EntityTable(AccessLevel.WriteOnly, "{数据库连接配置键}"})]
public class MyLog : LogEntity
{
    [EntityField(true, IsIdentity = true, IdentityNo = 100)]
    public long LogId { get; set; }

}

实体的Object类型成员

定义实体的成员属性可以包括:基础类型(byte、bool、short、int、long、float、double、decimal、string和枚举等),以及EntityChangeEvent子类对象,CacheList列表和CacheDictionary字典等类型;

基础类型的成员属性的定义就不详细介绍了,这里主要介绍下如何定义自定的对象类型成员;

EntityChangeEvent子类对象

它作为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;
    }
}

CacheList列表对象

同以上背包示例,修改成列表结构如下:

[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;
    }
}

CacheDictionary字典对象

同以上背包示例,修改成字典结构如下:

[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),负责对实体的增册改查操作;

⚠️ **GitHub.com Fallback** ⚠️