.NET 8 编写 LiteDB vs SQLite 数据库 CRUD 接口性能测试(准备篇)

2023-12-16

项目准备

此处还是以默认的 WeatherForecast (天气预报) 的数据为例,分别对两种类型的数据库做相应的 crud 操作,并对比测试性能。

1、.net cli 创建项目

这里我们使用的 .net8 版本, .net cli 创建 WebAppDbTest 项目,执行命令如下:

dotnet new webapi -o WebAppDbTest --no-https -f net8.0

2、nuget 包引用和项目结构

2.1、项目添加相关 nuget 包

  <ItemGroup>
    <PackageReference Include="FreeSql" Version="3.2.805" />
    <PackageReference Include="FreeSql.Provider.Sqlite" Version="3.2.805" />
    <PackageReference Include="FreeSql.Repository" Version="3.2.805" />
    <PackageReference Include="LiteDB.Async" Version="0.1.7" />
    <PackageReference Include="Mapster.Async" Version="2.0.1" />
    <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
    <PackageReference Include="Serilog.Sinks.LiteDB" Version="1.0.29" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

2.2、WebAppDbTest 项目结构

左边部分为 Nuget 安装的依赖包,右边部分为项目整体目录结构。

WebAppDbTest

3、项目代码说明

3.1、CSharp/C# 类文件说明

1、控制器类( Controllers

  • LiteDbController.cs ,针对 LiteDb 数据库的 CRUD 方法;
  • SqliteController.cs ,针对 SQLite 数据库的 CRUD 方法;
  • WeatherForecastController.cs (项目默认的类);

2、模型类( Models

  • ActionExecTime.cs ,记录方法执行时间;
  • AppLogs.cs ,记录日志信息;
  • WeatherForecast.cs ,天气预报数据模型;

3、服务类( Services

  • AppLogsServices.cs ,提供日志写入相关方法;
using System.Text.Json;
using WebAppDbTest.Models;

namespace WebAppDbTest.Services;

/// <summary>
/// 接口规范定义
/// </summary>
public interface IAppLogsServices
{
    /// <summary>
    /// 写入日志信息
    /// </summary>
    /// <param name="logs"></param>
    /// <param name="logLevel"></param>
    /// <returns></returns>
    Task WriteLogAsync(AppLogs logs, LogLevel logLevel = LogLevel.Information);

    /// <summary>
    /// 模型数据序列化json字符串
    /// </summary>
    /// <typeparam name="TData"></typeparam>
    /// <param name="data"></param>
    /// <returns></returns>
    Task<string> JsonSerializeAsync<TData>(TData data);
}

/// <summary>
/// 接口规范实现
/// </summary>
public class AppLogsServices : IAppLogsServices
{
    #region 构造函数 DI
    private readonly ILogger<AppLogsServices> _logger;

    public AppLogsServices(ILogger<AppLogsServices> logger)
    {
        _logger = logger;
    }
    #endregion

    /// <summary>
    /// 写入日志信息
    /// </summary>
    /// <param name="logs"></param>
    /// <param name="logLevel"></param>
    /// <returns></returns>
    public async Task WriteLogAsync(AppLogs logs, LogLevel logLevel = LogLevel.Information)
    {
        logs.LogLevel = logLevel;
        string jsonLogs = await JsonSerializeAsync(logs);

        switch (logLevel)
        {
            case LogLevel.Trace:
                _logger.LogTrace(jsonLogs);
                break;
            case LogLevel.Debug:
                _logger.LogDebug(jsonLogs);
                break;
            case LogLevel.Information:
                _logger.LogInformation(jsonLogs);
                break;
            case LogLevel.Warning:
                _logger.LogWarning(jsonLogs);
                break;
            case LogLevel.Error:
                _logger.LogError(jsonLogs);
                break;
            case LogLevel.Critical:
                _logger.LogCritical(jsonLogs);
                break;
            case LogLevel.None:
                _logger.LogInformation(jsonLogs);
                break;
            default:
                _logger.LogInformation(jsonLogs);
                break;
        }
    }

    /// <summary>
    /// json 序列化
    /// </summary>
    /// <typeparam name="TData"></typeparam>
    /// <param name="data"></param>
    /// <returns></returns>
    public async Task<string> JsonSerializeAsync<TData>(TData data)
    {
        var options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        };
        await using var stream = new MemoryStream();
        await JsonSerializer.SerializeAsync(stream, data, options);
        stream.Position = 0;
        using var reader = new StreamReader(stream);
        return await reader.ReadToEndAsync();
    }
}
  • WeatherForecastServices.cs ,模拟天气预报的数据;
using LiteDB;
using LiteDB.Async;
using Mapster;
using System.Diagnostics;
using System.Linq.Expressions;
using WebAppDbTest.Models;

namespace WebAppDbTest.Services;

/// <summary>
/// 天气预报接口规范定义
/// </summary>
public interface IWeatherForecastServices
{
    /// <summary>
    /// 获取天气预报概要
    /// </summary>
    /// <returns></returns>
    string GetSummarie();

    /// <summary>
    /// 获取天气预报列表
    /// </summary>
    /// <param name="count"></param>
    /// <returns></returns>
    IEnumerable<WeatherForecast> GetWeatherForecasts(int count);

    #region about litedb crud
    Task<Guid> LiteDbAddSingleAsync<T>(string collectioName, T t);

    Task<int> LiteDbAddBulkAsync<T>(string collectioName, IEnumerable<T> list);

    Task<T> LiteDbGetSingleAsync<T>(string collectioName, Guid id);

    Task<IEnumerable<T>> LiteDbGetAllAsync<T>(string collectioName);

    Task<bool> LiteDbUpdateSingleAsync<T>(string collectioName, T t);

    Task<int> LiteDbUpdateBulkAsync<T>(string collectioName, IEnumerable<T> list);

    Task<bool> LiteDbDeleteSingleAsync<T>(string collectioName, Guid id);

    Task<int> LiteDbDeleteBulkAsync<T>(string collectioName, Expression<Func<T, bool>> predicate);
    #endregion

    #region about sqlite crud
    Task<Guid> SqliteAddSingleAsync<T>(T t) where T : BaseEntity;

    Task<int> SqliteAddBulkAsync<T>(IEnumerable<T> list) where T : BaseEntity;

    Task<T> SqliteGetSingleAsync<T>(Guid id) where T : BaseEntity;

    Task<IEnumerable<T>> SqliteGetAllAsync<T>() where T : BaseEntity;

    Task<bool> SqliteUpdateSingleAsync<T>(T t) where T : BaseEntity, new();

    Task<int> SqliteUpdateBulkAsync<T>(IEnumerable<T> list) where T : BaseEntity, new();

    Task<bool> SqliteDeleteSingleAsync<T>(Guid id) where T : BaseEntity;

    Task<int> SqliteDeleteBulkAsync<T>(List<Guid> ids) where T : BaseEntity;
    #endregion
}

/// <summary>
/// 天气预报接口规范实现,模拟天气预报的数据
/// </summary>
public class WeatherForecastServices : IWeatherForecastServices
{
    #region 构造函数 DI
    private readonly IAppLogsServices _logger;
    private readonly IConfiguration _configuration;
    private readonly IFreeSql _freeSql;
    private readonly IWebHostEnvironment _webHostEnvironment;

    public WeatherForecastServices(IAppLogsServices logger,
        IConfiguration configuration,
        IFreeSql freeSql,
        IWebHostEnvironment webHostEnvironment)
    {
        _logger = logger;
        _configuration = configuration;
        _freeSql = freeSql;
        _webHostEnvironment = webHostEnvironment;
    }
    #endregion

    #region 模拟数据
    /// <summary>
    /// 模拟天气情况摘要数据列表
    /// </summary>
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public string GetSummarie() => Summaries[Random.Shared.Next(Summaries.Length)];

    public IEnumerable<WeatherForecast> GetWeatherForecasts(int count)
    {
        if (count <= 0 || count > 1000) count = 1000;

        /** 等效代码如下
        return Enumerable.Range(1, count).Select(index => {
            int temperatureC = Random.Shared.Next(-20, 55);
            var wf = new WeatherForecast
            {
                Id = Guid.NewGuid(),
                //Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Date = DateTime.Now.AddDays(index),
                TemperatureC = temperatureC,
                TemperatureF = 32 + (int)(temperatureC / 0.5556),
                Summary = GetSummarie()
            };
            return wf;
        }).ToArray();
        */

        return Enumerable.Range(1, count).Select(index => GetWeatherForecast(index)).ToArray();
    }

    private WeatherForecast GetWeatherForecast(int index) 
    {
        int temperatureC = Random.Shared.Next(-20, 55);
        var wf = new WeatherForecast
        {
            Id = Guid.NewGuid(),
            Date = DateTime.Now.AddDays(index),
            TemperatureC = temperatureC,
            TemperatureF = 32 + (int)(temperatureC / 0.5556),
            Summary = GetSummarie()
        };
        return wf;
    }

    #endregion

    private enum DbFileType { LiteDB, SQLite };

    private string GetConnString(int index, DbFileType dbFileType = DbFileType.LiteDB) 
    {
        string? dbFile = _configuration.GetSection($"DbConfig:{index}:DbFilePath").Value;
        string filePath = Path.Combine(_webHostEnvironment.ContentRootPath, dbFile);

        string dbConnString = string.Empty;
        switch (dbFileType)
        {
            case DbFileType.LiteDB:
                dbConnString = $"Filename={ filePath };Connection=shared;Password=123456";
                break;
            case DbFileType.SQLite:
                dbConnString = $"Data Source={ filePath };Version=3;Pooling=False;Max Pool Size=100";
                break;
            default:
                dbConnString = $"Filename={ filePath };Connection=shared;Password=123456";
                break;
        }

        return dbConnString;
    }

    private static readonly Stopwatch _sw = new();

    /// <summary>
    /// 记录信息
    /// </summary>
    /// <param name="ts">方法执行耗时,单位:毫秒/ms</param>
    /// <param name="appLogs"></param>
    /// <returns></returns>
    private async Task LiteDbWraiteInfoAsync(ActionExecInfo actionExecInfo, AppLogs appLogs)
    {
        // 记录操作方法执行的时间
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<ActionExecInfo>(nameof(ActionExecInfo));
        var item = await collection.InsertAsync(actionExecInfo);
        appLogs.ActionExecInfoId = item.AsGuid;

        // 记录日志
        await _logger.WriteLogAsync(appLogs);
    }

    #region About LiteDb CRUD
    public async Task<Guid> LiteDbAddSingleAsync<T>(string collectioName, T t)
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        var item = await collection.InsertAsync(t);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "AddSingle",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "AddSingle",
            ItemCount = 1,
            OperationInfo = $"[AddSingle] ==> 插入数据:1条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return item.AsGuid;
    }

    public async Task<int> LiteDbAddBulkAsync<T>(string collectioName, IEnumerable<T> list)
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个表和 MongoDB 一样的
        var collection = db.GetCollection<T>(collectioName);
        int rcount = await collection.InsertBulkAsync(list);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "AddBulk",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "AddBulk",
            ItemCount = 1,
            OperationInfo = $"[AddBulk] ==> 插入数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount;
    }

    public async Task<T> LiteDbGetSingleAsync<T>(string collectioName, Guid id) 
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        var result = await collection.FindByIdAsync(id); // 下面代码等效
        // var item = await collection.FindOneAsync(x => x.Id == id);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "GetSingle",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "GetSingle",
            ItemCount = 1,
            OperationInfo = $"[GetSingle] ==> 查询数据:1条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return result;
    }

    public async Task<IEnumerable<T>> LiteDbGetAllAsync<T>(string collectioName)
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        var result = await collection.FindAllAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "GetAll",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "GetAll",
            ItemCount = result.Count(),
            OperationInfo = $"[GetAll] ==> 查询数据:{result.Count()}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return result;
    }

    public async Task<bool> LiteDbUpdateSingleAsync<T>(string collectioName, T t) 
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        bool isOk = await collection.UpdateAsync(t);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "UpdateSingle",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "UpdateSingle",
            ItemCount = 1,
            OperationInfo = $"[UpdateSingle] ==> 更新数据:1条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return isOk;
    }

    public async Task<int> LiteDbUpdateBulkAsync<T>(string collectioName, IEnumerable<T> list)
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        int rcount = await collection.UpdateAsync(list);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "UpdateBulk",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "UpdateBulk",
            ItemCount = rcount,
            OperationInfo = $"[UpdateBulk] ==> 更新数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount;
    }

    public async Task<bool> LiteDbDeleteSingleAsync<T>(string collectioName, Guid id) 
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        bool isOk = await collection.DeleteAsync(id);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "DeleteSingle",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "DeleteSingle",
            ItemCount = 1,
            OperationInfo = $"[DeleteSingle] ==> 删除数据:1条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return isOk;
    }

    public static BsonValue Serialize(Guid id) => new BsonDocument(new Dictionary<string, BsonValue>
    {
        {"_id", id }
    });

    public static Guid Deserialize(BsonValue bsonValue)
    {
        var id = bsonValue["_id"].AsGuid;
        return id;
    }

    public async Task<int> LiteDbDeleteBulkAsync<T>(string collectioName, Expression<Func<T, bool>> predicate) 
    {
        _sw.Start();
        string connectionString = GetConnString(0);
        //打开数据库,如果不存在会自动创建。
        using var db = new LiteDatabaseAsync(connectionString);
        //打开一个集合和 MongoDB 一样的,类似关系数据库的表。
        var collection = db.GetCollection<T>(collectioName);
        //int rcount = await collection.DeleteAllAsync();
        int rcount = await collection.DeleteManyAsync(predicate);
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "DeleteBulk",
            ExecTime = ts,
            Database = "litedb"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "DeleteBulk",
            ItemCount = rcount,
            OperationInfo = $"[DeleteBulk] ==> 删除数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount;

    }
    #endregion


    #region About SQLite CRUD
    public async Task<Guid> SqliteAddSingleAsync<T>(T t) where T : BaseEntity
    {
        _sw.Start();
        var rcount = await _freeSql.Insert(t).ExecuteAffrowsAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "AddSingle",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "AddSingle",
            ItemCount = rcount,
            OperationInfo = $"[AddSingle] ==> 插入数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return t.Id;
    }

    public async Task<int> SqliteAddBulkAsync<T>(IEnumerable<T> list) where T : BaseEntity
    {
        _sw.Start();
        int rcount = await _freeSql.Insert(list).ExecuteAffrowsAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "AddBulk",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "AddBulk",
            ItemCount = 1,
            OperationInfo = $"[AddBulk] ==> 插入数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount;
    }

    public async Task<T> SqliteGetSingleAsync<T>(Guid id) where T : BaseEntity
    {
        _sw.Start();
        var result = await _freeSql.Select<T>().Where(x => x.Id == id).FirstAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "GetSingle",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "GetSingle",
            ItemCount = 1,
            OperationInfo = $"[GetSingle] ==> 查询数据:1条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return result;
    }

    public async Task<IEnumerable<T>> SqliteGetAllAsync<T>() where T : BaseEntity
    {
        _sw.Start();
        var result = await _freeSql.Select<T>().ToListAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "GetAll",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "GetAll",
            ItemCount = result.Count(),
            OperationInfo = $"[GetAll] ==> 查询数据:{result.Count()}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return result;
    }

    public async Task<bool> SqliteUpdateSingleAsync<T>(T t) where T : BaseEntity, new()
    {
        _sw.Start();

        // 推荐快照模式
        var repo = _freeSql.GetRepository<T>();
        var item = new T { Id = t.Id };
        repo.Attach(item); //此时快照 item
        t.Adapt(item);

        //bool isOk = ReferenceEquals(item, t);
        int rcount = await repo.UpdateAsync(item); //对比快照时的变化

        // 传统模式
        // int rcount = await _freeSql.Update<T>().SetSource(t).IgnoreColumns(a => new { a.Id }).ExecuteAffrowsAsync();

        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "UpdateSingle",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "UpdateSingle",
            ItemCount = rcount,
            OperationInfo = $"[UpdateSingle] ==> 更新数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount > 0;
    }

    public async Task<int> SqliteUpdateBulkAsync<T>(IEnumerable<T> list) where T : BaseEntity, new()
    {
        _sw.Start();

        // 推荐快照模式
        var repo = _freeSql.GetRepository<T>();
        var items = list.Select(x => new T{ Id = x.Id });
        repo.Attach(items); //此时快照 item
        //list.Adapt(items);
        items = list;
        bool isOk = ReferenceEquals(items, list);
        int rcount = await repo.UpdateAsync(items); //对比快照时的变化

        // 传统模式
        //int rcount = await _freeSql.Update<T>().SetSource(list).IgnoreColumns(a => new { a.Id }).ExecuteAffrowsAsync();

        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "UpdateBulk",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "UpdateBulk",
            ItemCount = rcount,
            OperationInfo = $"[UpdateBulk] ==> 更新数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount;
    }

    public async Task<bool> SqliteDeleteSingleAsync<T>(Guid id) where T : BaseEntity
    {
        _sw.Start();
        int rcount = await _freeSql.Delete<T>().Where(x => x.Id == id).ExecuteAffrowsAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "DeleteSingle",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "DeleteSingle",
            ItemCount = rcount,
            OperationInfo = $"[DeleteSingle] ==> 删除数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount > 0;
    }

    public async Task<int> SqliteDeleteBulkAsync<T>(List<Guid> ids) where T : BaseEntity
    {
        _sw.Start();
        int rcount = await _freeSql.Delete<T>(ids.ToArray()).ExecuteAffrowsAsync();
        _sw.Stop();
        TimeSpan ts = _sw.Elapsed;

        // 记录操作方法执行的时间
        var actionExecInfo = new ActionExecInfo
        {
            ActionName = "DeleteBulk",
            ExecTime = ts,
            Database = "sqlite"
        };

        // 记录日志
        var appLogs = new AppLogs
        {
            Label = "DeleteBulk",
            ItemCount = rcount,
            OperationInfo = $"[DeleteBulk] ==> 删除数据:{rcount}条,耗时:{ts.TotalMilliseconds}ms."
        };
        await LiteDbWraiteInfoAsync(actionExecInfo, appLogs);

        return rcount;
    }
    #endregion
}

4、程序入口类

  • Program.cs
using Serilog;
using WebAppDbTest.Services;

var builder = WebApplication.CreateBuilder(args);

//const string OUTPUT_TEMPLATE = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} <{ThreadId}> [{Level:u3}] {Message:lj}{NewLine}{Exception}";
const string OUTPUT_TEMPLATE = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}";
char b = Path.DirectorySeparatorChar; // 符号 

// creates custom collection `applog`
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .CreateLogger();

#region Host
builder.Host.ConfigureAppConfiguration((context, config) => {
    string configPath = $"{context.HostingEnvironment.ContentRootPath}{b}AppData{b}Configuration";
    config.SetBasePath(configPath)
      .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
      .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true)
      .AddEnvironmentVariables();
}).UseSerilog((context, logger) => {
    string liteDbPath = Path.Combine(context.HostingEnvironment.ContentRootPath, $"AppData{b}DataBase{b}LiteDbLogs.db");
    logger.WriteTo.LiteDB(liteDbPath, logCollectionName: "applog");
    logger.WriteTo.Console(outputTemplate: OUTPUT_TEMPLATE);
});
// .UseSerilog(Log.Logger, dispose: true);
#endregion

#region Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 注册 AppLogsServices
builder.Services.AddScoped<IAppLogsServices, AppLogsServices>();
// 注册 WeatherForecastServices
builder.Services.AddScoped<IWeatherForecastServices, WeatherForecastServices>();

// 注入 Sqlite 类型的 IFreeSql 
//string sqlitePath = $"AppData{b}DataBase{b}SQLiteTest.db";
string sqlitePath = builder.Configuration.GetSection("DbConfig:1:DbFilePath").Value;
string connStr = $"Data Source={Path.Combine(builder.Environment.ContentRootPath, sqlitePath)};Version=3;Pooling=False;Max Pool Size=100";
// Log.Logger.Information(connStr);

IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(dataType: FreeSql.DataType.Sqlite, connectionString: connStr)
    .UseAutoSyncStructure(false) //自动同步实体结构【开发环境必备】,FreeSql不会扫描程序集,只有CRUD时才会生成表。
    //.UseMonitorCommand(cmd => Console.Write(cmd.CommandText)) 
    .UseMonitorCommand(cmd => Log.Logger.Information(cmd.CommandText))
    .Build(); //请务必定义成 Singleton 单例模式
builder.Services.AddSingleton(fsql); 
#endregion

var app = builder.Build();

#region Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();
app.MapControllers(); 
#endregion

app.Run();

3.2、json 配置文件说明

  • appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "DbConfig": [
    {
      "DbType": "LiteDB",
      "DbFilePath": "AppData\\DataBase\\LiteDbTest.db"
    },
    {
      "DbType": "SQLite",
      "DbFilePath": "AppData\\DataBase\\SqliteTest.db"
    }
  ]
}

相关文件代码此处就不再详细说明,感兴趣的可自行查看项目地址:

  • WebAppDbTest https://gitee.com/dolayout/sample/tree/master/code/Sample.WebAppDbTest

4、项目运行预览

  • 启动 WebAppDbTest swagger 页面显示如下:

webapp

  • LiteDB & Sqlite 对应的 CRUD 方法:

webapp-dbtest-crud

数据库 .db 文件准备

1、创建 SQLite 数据库

请访问 SQLite 下载页面,从 Windows 区下载预编译的二进制文件。

1.1、在 Windows 上安装 SQLite

  • SQLite 下载, https://www.sqlite.org/download.html
    sqlite-download

此处我是 Windows 11 x64 环境,下载文件分别如下:

  • sqlite-dll-win-x64-3440200.zip
  • sqlite-tools-win-x64-3440200.zip

把下载文件拷贝到 D 盘并解压文件,如下所示:

sqlite

文件夹默认文件说明:

  • sqlite-dll-win-x64-3440200 文件夹默认包含: sqlite3.def sqlite3.dll 文件;
  • sqlite-tools-win-x64-3440200 文件夹默认包含: sqldiff.exe sqlite3.exe sqlite3_analyzer.exe 文件;

可以把 D:\sqlite\sqlite-tools-win-x64-3440200 添加到 PATH 环境变量,最后在命令提示符下,使用 sqlite3 命令,此处我就不添加环境变量了,直接双击 sqlite.exe 文件将显示如下结果:

sqlite-tools-win-x64

1.2、创建 SQLite 数据库

依据终端提示信息,输入命令创建数据库 SQLiteTest.db 文件,执行如下:

sqlite> .open SQLiteTest.db

查看 sqlite 更多命令帮助信息:

SQLite version 3.44.2 2023-11-24 11:41:44 (UTF-16 console I/O)
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open SQLiteTest.db
sqlite> PRAGMA key = '123456';
sqlite> .help
.archive ...             Manage SQL archives
.auth ON|OFF             Show authorizer callbacks
.backup ?DB? FILE        Backup DB (default "main") to FILE
.bail on|off             Stop after hitting an error.  Default OFF
.cd DIRECTORY            Change the working directory to DIRECTORY
.changes on|off          Show number of rows changed by SQL
.check GLOB              Fail if output since .testcase does not match
.clone NEWDB             Clone data into NEWDB from the existing database
.connection [close] [#]  Open or close an auxiliary database connection
.crnl on|off             Translate \n to \r\n.  Default ON
.databases               List names and files of attached databases
.dbconfig ?op? ?val?     List or change sqlite3_db_config() options
.dbinfo ?DB?             Show status information about the database
.dump ?OBJECTS?          Render database content as SQL
.echo on|off             Turn command echo on or off
.eqp on|off|full|...     Enable or disable automatic EXPLAIN QUERY PLAN
.excel                   Display the output of next command in spreadsheet
.exit ?CODE?             Exit this program with return-code CODE
.expert                  EXPERIMENTAL. Suggest indexes for queries
.explain ?on|off|auto?   Change the EXPLAIN formatting mode.  Default: auto
.filectrl CMD ...        Run various sqlite3_file_control() operations
.fullschema ?--indent?   Show schema and the content of sqlite_stat tables
.headers on|off          Turn display of headers on or off
.help ?-all? ?PATTERN?   Show help text for PATTERN
.import FILE TABLE       Import data from FILE into TABLE
.indexes ?TABLE?         Show names of indexes
.limit ?LIMIT? ?VAL?     Display or change the value of an SQLITE_LIMIT
.lint OPTIONS            Report potential schema issues.
.load FILE ?ENTRY?       Load an extension library
.log FILE|on|off         Turn logging on or off.  FILE can be stderr/stdout
.mode MODE ?OPTIONS?     Set output mode
.nonce STRING            Suspend safe mode for one command if nonce matches
.nullvalue STRING        Use STRING in place of NULL values
.once ?OPTIONS? ?FILE?   Output for the next SQL command only to FILE
.open ?OPTIONS? ?FILE?   Close existing database and reopen FILE
.output ?FILE?           Send output to FILE or stdout if FILE is omitted
.parameter CMD ...       Manage SQL parameter bindings
.print STRING...         Print literal STRING
.progress N              Invoke progress handler after every N opcodes
.prompt MAIN CONTINUE    Replace the standard prompts
.quit                    Stop interpreting input stream, exit if primary.
.read FILE               Read input from FILE or command output
.recover                 Recover as much data as possible from corrupt db.
.restore ?DB? FILE       Restore content of DB (default "main") from FILE
.save ?OPTIONS? FILE     Write database to FILE (an alias for .backup ...)
.scanstats on|off|est    Turn sqlite3_stmt_scanstatus() metrics on or off
.schema ?PATTERN?        Show the CREATE statements matching PATTERN
.separator COL ?ROW?     Change the column and row separators
.session ?NAME? CMD ...  Create or control sessions
.sha3sum ...             Compute a SHA3 hash of database content
.shell CMD ARGS...       Run CMD ARGS... in a system shell
.show                    Show the current values for various settings
.stats ?ARG?             Show stats or turn stats on or off
.system CMD ARGS...      Run CMD ARGS... in a system shell
.tables ?TABLE?          List names of tables matching LIKE pattern TABLE
.timeout MS              Try opening locked tables for MS milliseconds
.timer on|off            Turn SQL timer on or off
.trace ?OPTIONS?         Output each SQL statement as it is run
.version                 Show source, library and compiler versions
.vfsinfo ?AUX?           Information about the top-level VFS
.vfslist                 List all available VFSes
.vfsname ?AUX?           Print the name of the VFS stack
.width NUM1 NUM2 ...     Set minimum column widths for columnar output
sqlite>

此时在当前目录下, SQLite 的数据库文件 SQLiteTest.db 文件就创建好了。

接下来使用 dbeaver-ce 工具连接数据库文件测试:

连接测试

  • sqlite 数据表脚本:
-- WeatherForecast definition

CREATE TABLE "WeatherForecast" (  
  "Id" CHARACTER(36) NOT NULL, 
  "Date" TEXT NOT NULL, 
  "TemperatureC" INTEGER NOT NULL, 
  "TemperatureF" INTEGER NOT NULL, 
  "Summary" NVARCHAR(255), 
  PRIMARY KEY ("Id")
);

2、创建 LiteDB 数据库

2.1、LiteDB.Shell

LiteDB 项目包含一个简单的控制台应用程序 ( LiteDB.Shell.exe ),可用于查看、更新以及测试你的数据,在处理你的数据库时非常有用。

  • LiteDB.Shell 项目地址, https://github.com/mustakimali/LiteDB.Shell.NetCore

2.2、创建 LiteDB 数据库

使用 LiteDB.Shell 创建数据库,执行如下命令:

> open <filename>|<connectionString>
    Open/Crete a new database

基本 Shell 命令,尝试使用 help full 执行所有命令:

Basic Shell Commands - try `help full` for all commands
=======================================================
> open <filename>|<connectionString>
    Open/Crete a new database

> show collections
    List all collections inside database

> db.<collection>.insert <jsonDoc>
    Insert a new document into collection

> db.<collection>.update <jsonDoc>
    Update a document inside collection

> db.<collection>.delete <filter>
    Delete documents using a filter clausule (see find)

> db.<collection>.find <filter> [skip N][limit N]
    Show filtered documents based on index search

> db.<collection>.count <filter>
    Show count rows according query filter

> db.<collection>.ensureIndex <field> [true|{options}]
    Create a new index document field. For unique key, use true

> db.<collection>.indexes
    List all indexes in this collection

<filter> = <field> [=|>|>=|<|<=|!=|like|between] <jsonValue>
    Filter query syntax

<filter> = (<filter> [and|or] <filter> [and|or] ...)
    Multi queries syntax

Try:
 > db.customers.insert { _id:1, name:"John Doe", age: 37 }
 > db.customers.ensureIndex name
 > db.customers.find name like "John"
 > db.customers.find name like "John" and _id between [0, 100] limit 10

说明: litedb 数据库和数据集无需创建,当不存在时执行 crud 代码会自动创建。

好了先到这里,我们就把测试项目准备好了,关于接口测试性能对比,下篇再续,敬请观看。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

.NET 8 编写 LiteDB vs SQLite 数据库 CRUD 接口性能测试(准备篇) 的相关文章

  • C# 无法访问已释放的对象

    我正在制作一个服务器 客户端应用程序 我将服务器套接字设置为侦听 并设置 BeginAccept 方法 当我关闭服务器套接字 Socket Close 以关闭服务器时 BeginAccept 方法的异步回调方法抛出异常 我检查了异常 发现异
  • MVC4更新部分视图

    我正在开发一个简单的 MVC 应用程序 我有主视图 部分视图和控制器 这是我的主要视图 model partitalViewTest Models Qset div class transbox style height 1 Html Pa
  • 从标签 ID 更改整个 gridview 单元格颜色

    试图弄清楚当我有标签 ID 时如何更改 gridview 单元格的背景颜色
  • 为什么 Application.Exit 无法工作?

    我有一个应用程序在取消对话框时出现奇怪的错误 如果该框被取消 应用程序将无法继续 因此它会退出 但由于某种原因它无法工作 因此它会继续运行并崩溃 我调试了这个问题 并且不知何故应用程序在 Application Exit 调用之后运行 我正
  • Response.Redirect() 将绝对 URL 作为相对 URL 处理

    我有一个 net C 页面 它重定向到绝对 url 例如 Response Redirect rtsp myvideoServer com myVideoAddress mp4 ticket 1234 dt 1234 但重定向后会导致 ht
  • 如何在 SqlDataReader.Read() 期间从死锁异常中恢复

    我的 NET 应用程序的事件日志显示 它在从 Sql Server 读取数据时偶尔会出现死锁 这种情况通常非常罕见 因为我们已经优化了查询以避免死锁 但有时仍然会发生 过去 我们在调用ExecuteReader函数在我们的SqlComman
  • 类似于 .NET Framework 2.0 的 MEF(托管可扩展性框架)

    我在自己的项目中使用了 MEF 并且非常喜欢它 这很容易 在弄清楚我们的awkwardAPI模型 它刚刚工作了 现在我需要 NET Framework 2 0 类似的东西 有没有可以在 NET Framework 2 0 下工作的类似项目
  • Azure CloudTable 线程安全吗?

    我正在使用 Storage SDK 2 0 从不同线程 ASP NET 应用程序 写入 Azure 表存储 Is 云表 object 线程安全 我是否可以仅初始化 CloudStorageAccount CloudTableClient 和
  • C# 中的 strstr() 等效项

    我有两个byte 我想找到第二个的第一次出现byte 在第一个byte 或其中的一个范围 我不想使用字符串来提高效率 翻译第一个byte to a string会效率低下 基本上我相信就是这样strstr 在 C 中做 最好的方法是什么 这
  • 当进程等待完成时如何显示加载控件?

    我决定使用这个第三方组件在我的 Windows 窗体中制作一个简单的加载控件 http www codeproject com Articles 14841 How to write a loading circle animation i
  • idleTimeout 和 ShutdownTimeout 之间的区别

    我正在尝试放宽网站的会话过期策略 以便用户可以指定会话超时间隔 我需要弄清楚应该指定哪些网站相关设置 以免过多限制用户 例如 可能需要 1 天的间隔 我将使用门票来实现这一点 现在 我知道我可以在网站的 web config 文件中指定id
  • StringComparison.InvariantCultureIgnoreCase 去哪儿了?

    我正在将 C 代码移植到 Windows 应用商店应用程序 令我惊讶的是 以下代码不再起作用 someString Equals someOtherString StringComparison InvariantCultureIgnore
  • 如何在 C# 中向类、方法、属性等添加文档工具提示?

    不确定我的说法是否正确 但我想开始向我的类 方法 属性等添加一些文档 我know这可能是非常明显的 但我从未真正学会过 我不知道从哪里开始 只是为了澄清 每当您滚动某个类 或方法 属性等 时 它都会在 Visual Studio 中显示一个
  • default(CancellationToken) 如何有对应的 CancellationTokenSource

    当我创建默认值时CancellationToken我可以在调试器中看到CancellationToken has a CancellationTokenSource与其关联的存储在私有中m source field 我想知道对于结构来说怎么
  • 与 SQL 中的 IN 运算符相反

    我怎么能做相反的事情 换句话说 选择所有姓氏不是 Hansen 或 Pettersen 的人 WHERE lastname NOT IN Hansen Pettersen 请参阅 IN 和 NOT IN 运算符 部分SQLite 所理解的
  • WCF WebHttp 混合身份验证(基本和匿名)

    所有这些都与 WebHttp 绑定有关 托管在自定义服务主机中 IIS 目前不是一个选项 我已经实现了自定义 UserNamePasswordValidator 和自定义 IAuthorizationPolicy 当我将端点的绑定配置为使用
  • 记录共享和映射的诊断上下文

    据我所知 其他人做了什么来解决 Commons Logging 项目 针对 NET 和 Java 不支持映射或嵌套诊断上下文这一事实 执行摘要 我们选择直接使用实现者日志框架 在我们的例子中为 log4j 长答案 您是否需要一个抽象日志框架
  • Asp.net core默认路由

    简化版Startup code public void ConfigureServices IServiceCollection services services AddMvc public void Configure IApplica
  • MSChart 控件中的自定义 X/Y 网格线

    我有一个带有简单 2D 折线图的 C Windows 窗体 我想向其中添加自定义 X 或 Y 轴标记 并绘制自定义网格线 例如 以突出显示的颜色 虚线 我查看了 customLabels 属性 但这似乎覆盖了我仍然想显示的默认网格 这是为了
  • 如何使用 VB.NET 打开受密码保护的共享网络文件夹?

    我需要在网络上打开受密码保护的共享文件夹才能访问 Access 97 数据库 如何打开文件夹并输入密码 在这里找到http www mredkj com vbnet vbnetmapdrive html http www mredkj co

随机推荐

  • 软件测试/测试开发丨人工智能算法的基本原理,如何解决实际的问题

    人工智能 AI 算法的基本原理涉及模仿人类智能行为的计算机程序和模型 这些算法通常通过学习和适应从数据中提取规律来解决实际问题 以下是一些常见的人工智能算法以及它们的基本原理 监督学习算法 图像识别 问题 识别图像中的数字 算法 使用卷积神
  • 【UE】在蓝图中修改材质实例的参数的两种方式

    目录 方式一 通过 在材质上设置标量 向量参数值 节点实现 方式二 通过 设置标量 向量参数值 节点实现 方式一 通过 在材质上设置标量 向量参数值 节点实现 1 在材质中设置了两个参数 2 创建材质实例 3 创建一个蓝图 对静态网格体赋予
  • 【UE】制作物体逐渐溶解消失并且可以复原的效果

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加第三人称游戏和初学者内容包资源到内容浏览器 2 找到并打开初学者内容包中椅子的材质 M Chair 将混合模式改为 已遮罩 在材质图表中添加如下节点 此时我们就可以通过参数 Fade
  • 【UE 材质】角色触碰空气墙效果

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加一个第三人称游戏资源到内容浏览器 2 新建一个材质参数集 这里命名为 MPC Vector 打开 MPC Vector 添加一个向量参数 3 新建一个材质 这里命名为 M Wall
  • 【UE5.1 MetaHuman】使用mixamo_converter把Mixamo的动画重定向给MetaHuman使用

    目录 前言 效果 步骤 一 下载mixamo converter软件 二 Mixamo动画重定向 三 导入UE 四 动画重定向 五 使用重定向后的动画 前言 上一篇 UE5 初识MetaHuman 创建虚拟角色 中我们已经制作了一个Meta
  • 【UE】制作熔岩星球材质

    效果 步骤 1 新建一个工程 创建一个Basic关卡 添加第三人称游戏和初学者内容包资源到内容浏览器 2 新建一个材质 这里命名为 M Sun 打开 M Sun 添加两个Texture节点 纹理分别为 T Rock Basalt N 和 T
  • 【UE5】监控摄像头效果(上)

    目录 效果 步骤 一 视角切换 二 摄像头画面后期处理 三 在场景中显示摄像头画面 效果 步骤 一 视角切换 1 新建一个Basic关卡 添加第三人称游戏资源到项目浏览器 2 新建一个Actor蓝图 这里命名为 BP SecurityCam
  • pico示波器使用

    文章目录 Pico示波器保存波形 Pico示波器录制数据 Pico示波器解析CAN报文 Pico示波器保存波形 Pico示波器可以通过以下步骤保存波形 在示波器上选择要保存的波形 连接示波器到计算机上 可以使用USB或者Ethernet连接
  • 头歌——HBase 开发:使用Java操作HBase

    第1关 创建表 题目 任务描述 本关任务 使用 Java 代码在 HBase 中创建表 相关知识 为了完成本关任务 你需要掌握 1 如何使用 Java 连接 HBase 数据库 2 如何使用 Java 代码在 HBase 中创建表 如何使用
  • 【UE5】瞬移+马赛克过渡效果

    效果 步骤 1 新建一个工程 创建一个Basic关卡 2 添加第三人称游戏资源到内容浏览器 3 新建一个材质 这里命名为 M Pixel 打开 M Pixel 设置材质域为 后期处理 在材质图表中添加如下节点 此时效果如下 已经有马赛克的效
  • 【3DsMax】制作简单的骨骼动画

    效果 步骤 首先准备4个板子模型展开放置好 添加一个4段的骨骼 选中其中的一块板子添加蒙皮命令 在蒙皮的参数面板中 设置每块板子对应哪块骨骼 设置好后你可以发现此时就已经可以通过骨骼来控制模型了 接下来就可以制作动画 点击左下角 时间配置
  • 【UE】制作地月全息投影

    效果 步骤 1 在必应国际版上搜索 purlin noise 下载如下所示图片 再搜索 Earth Map 下载如下所示图片 再搜索 Moon 360 下载如下所示图片 这三张图片的资源链接如下 链接 https pan baidu com
  • python在车载电子测试方面的应用笔记【1】

    文章目录 在DataFrame中某列插入数据 并根据另一列查找是否存在某个字符串完全一样 在另一列插入对应数据的功能 删除DataFrame某列数据长度大于6的数据 使用 PyInstaller 打包成一个独立的 exe 文件 通过检索空格
  • 通过kubeadm方式安装k8s

    虚拟机最少是 2 core master内存最小3G node内存最小2G 要求的Docker版本是18 03 如果不是安装的docker ce 版本是过旧的 可以选择删除后重新安装 也可以重新创建一个虚拟机执行以下命令 简单方法 使用ma
  • Docker build 无法解析域名

    报错 Docker build 无法解析域名 报错 ERROR 2 12 RUN curl o etc yum repos d CentOS Base repo https mirrors aliyun com repo Centos 7
  • 安装 运行 gemmini 和chipyard

    安装gemmini 和chipyard过程 安装版本 chipyard 版本是1 8 1 gemmini版本0 7 0 tip 如果在base里安装conda lock觉得缓慢 可以新建新的环境时就指定安装conda lock conda
  • GoLong的学习之路,进阶,微服务之序列化协议,Protocol Buffers V3

    这章是接上一章 使用 RPC包 序列化中没有详细去讲 因为这一块需要看的和学习的地方很多 并且这一块是RPC中可以说是最重要的一块 也是性能的重要影响因子 今天这篇主要会讲其使用方式 文章目录 Protocol Buffers V3 背景以
  • GoLong的学习之路,进阶,微服务之使用,RPC包(包括源码分析)

    今天这篇是接上上篇RPC原理之后这篇是讲如何使用go本身自带的标准库RPC 这篇篇幅会比较短 重点在于上一章对的补充 文章目录 RPC包的概念 使用RPC包 服务器代码分析 如何实现的 总结 Server还提供了两个注册服务的方法
  • 车载以太网笔记

    文章目录 以太网协议分层 协议 中间设备 子网掩码 物理层 测试 内容比较杂 后续会整理 以太网协议分层 协议 中间设备
  • .NET 8 编写 LiteDB vs SQLite 数据库 CRUD 接口性能测试(准备篇)

    WebAppDbTest 项目准备 项目准备 1 net cli 创建项目 2 nuget 包引用和项目结构 2 1 项目添加相关 nuget 包 2 2 WebAppDbTest 项目结构 3 项目代码说明