精华内容
参与话题
问答
  • Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。 从字面上我们可以我们...

    Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。

    从字面上我们可以我们可以把UnitOfWork叫做工作单元,从概念上它是协助代码块的事务。为什么我们需要用UnitOfWork?有人说EF不是的DbContext的SaveChanges不就有提交变更数据的功能吗?为什么还要多一层封装?是的,如果我们项目只是用EF的话,项目又会经常变更,不用考虑那么多我们可以直接用EF,但是如果我们在支持EF的同时还需要支持Redis、NHibernate或MongoDB呢?我们怎么做统一的事务管理?所以封装一个UnitOfWork是有必要的。类似的Repository也是一样,仓储Repository的功能其实就是EF的DbSet<T>,但是我们的数据库访问技术可能会改变,所以我们需要提供一层封装,来隔离领域层或应用层对数据访问层的依赖。那么ABP是怎么定义UnitOfWork的呢?

        public interface IUnitOfWork : IActiveUnitOfWork, IUnitOfWorkCompleteHandle
        {
            /// <summary>
            /// Begins the unit of work with given options.
            /// 开始 unit of work的一些配置UnitOfWorkOptions,主要是事务的级别,超时时间,配置文件等
            /// </summary>
            /// <param name="options">Unit of work options</param>
            void Begin(UnitOfWorkOptions options);
        }
    
        public interface IUnitOfWorkCompleteHandle : IDisposable
        {
            /// <summary>
            /// Completes this unit of work.
            /// It saves all changes and commit transaction if exists.
            /// 统一事务提交
            /// </summary>
            void Complete();
    
            /// <summary>
            /// Completes this unit of work.
            /// It saves all changes and commit transaction if exists.
            /// 异步的Complete方法
            /// </summary>
            Task CompleteAsync();
        }
    

    从接口的定义来看,UnitOfWork主要涉及到事务的提交,回滚操作这边没有再定义一个方法,因为作者用的是TransactionScope,失败了会自动回滚。当然有定义了Dispose方法。现在我们来看下UnitOfWork的实现方法,抽象类UnitOfWorkBase,我删除了一些跟本文无关的代码,方便阅读。

        public abstract class UnitOfWorkBase : IUnitOfWork
        {
            public UnitOfWorkOptions Options { get; private set; }
    
            /// <summary>
            /// 开始UnitOfWork的一些配置,和事务的初始化
            /// </summary>
            /// <param name="options"></param>
            public void Begin(UnitOfWorkOptions options)
            {
                if (options == null)
                {
                    throw new ArgumentNullException("options");
                }
    
                PreventMultipleBegin();
                Options = options; //TODO: Do not set options like that!
    
                SetFilters(options.FilterOverrides);
    
                BeginUow();
            }
    
            /// <summary>
            /// 事务的提交,异常的捕获
            /// </summary>
            public void Complete()
            {
                PreventMultipleComplete();
                try
                {
                    CompleteUow();
                    _succeed = true;
                    OnCompleted();
                }
                catch (Exception ex)
                {
                    _exception = ex;
                    throw;
                }
            }
    
            /// <summary>
            /// 结束事务,失败就回滚
            /// </summary>
            public void Dispose()
            {
                if (IsDisposed)
                {
                    return;
                }
    
                IsDisposed = true;
    
                if (!_succeed)
                {
                    OnFailed(_exception);
                }
    
                DisposeUow();
                OnDisposed();
            }
        }
    

    我们知道UnitOfWorkBase是抽象类,对于不同的数据访问技术方案我们要定义不用的工作单元实现类,比如EF和NHibernate的事务实现机制是不一样的,这里我们看下EfUnitOfWork

        public class EfUnitOfWork : UnitOfWorkBase, ITransientDependency
        {
            private readonly IDictionary<Type, DbContext> _activeDbContexts;
            private readonly IIocResolver _iocResolver;
            private TransactionScope _transaction;
    
            /// <summary>
            /// Creates a new <see cref="EfUnitOfWork"/>.
            /// </summary>
            public EfUnitOfWork(IIocResolver iocResolver, IUnitOfWorkDefaultOptions defaultOptions)
                : base(defaultOptions)
            {
                _iocResolver = iocResolver;
                _activeDbContexts = new Dictionary<Type, DbContext>();
            }
    
            protected override void BeginUow()
            {
                if (Options.IsTransactional == true)
                {
                    var transactionOptions = new TransactionOptions
                    {
                        IsolationLevel = Options.IsolationLevel.GetValueOrDefault(IsolationLevel.ReadUncommitted),
                    };
    
                    if (Options.Timeout.HasValue)
                    {
                        transactionOptions.Timeout = Options.Timeout.Value;
                    }
    
                    _transaction = new TransactionScope(
                        TransactionScopeOption.Required,
                        transactionOptions,
                        Options.AsyncFlowOption.GetValueOrDefault(TransactionScopeAsyncFlowOption.Enabled)
                        );
                }
            }
    
            public override void SaveChanges()
            {
                _activeDbContexts.Values.ForEach(SaveChangesInDbContext);
            }
    ...

    上面已经定义了UnitOfWork接口和实现方法,那我们改怎么使用呢?一般的我们的使用方式是这样的,下面的场景是模拟银行转账功能,从一个账户扣钱和另一个账户加钱。下面是领域层定义的账户转账服务,我们在整个操作实现完后调用 _unitOfWork.Commit()进行提交,在领域服务构造函数注入UnitOfWork。

        // 账号转账领域服务类
        public class AccountService
        {
            private readonly IAccountRepository _productRepository;
            private readonly IUnitOfWork _unitOfWork;
    
            public AccountService(IAccountRepository productRepository, IUnitOfWork unitOfWork)
            {
                _productRepository = productRepository;
                _unitOfWork = unitOfWork;            
            }
            
            public void Transfer(Account from, Account to, decimal amount)
            {
                if (from.Balance >= amount)
                {
                    from.Balance -= amount;
                    to.Balance += amount;
    
                    _productRepository.Save(from);
                    _productRepository.Save(to);
                    _unitOfWork.Commit();
                }
            } 
        }
    

    这样的设计简单易懂,但是我们每个提交都要引用UnitOfWork会比较麻烦,那么有没有更好的设计思路呢?ABP的设计思想还是比较值得借鉴的。ABP的UnitOfWork的设计思路还是沿用作者最喜欢的切面编程,何为切面编程:通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。也就是AOP技术,ABP作者用的是Castle Windsor来实现的。一般的我们需要两步,1、继承IInterceptor接口重写Intercept方法,这样我们就可以实现动态拦截方法了,2、那么我们到底怎么才能动态代理要拦截的方法呢?我们可以继承Attribute,自定义UnitOfWorkAttribute。可能你现在还不明白,那么我们来看下具体代码吧。

        internal class UnitOfWorkInterceptor : IInterceptor
        {
            private readonly IUnitOfWorkManager _unitOfWorkManager;
    
            public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager)
            {
                _unitOfWorkManager = unitOfWorkManager;
            }
    
            public void Intercept(IInvocation invocation)
            {
                if (_unitOfWorkManager.Current != null)
                {
                    //Continue with current uow
                    invocation.Proceed();
                    return;
                }
    
                var unitOfWorkAttr = UnitOfWorkAttribute.GetUnitOfWorkAttributeOrNull(invocation.MethodInvocationTarget);
                if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled)
                {
                    //No need to a uow
                    invocation.Proceed();
                    return;
                }
    
                //No current uow, run a new one
                PerformUow(invocation, unitOfWorkAttr.CreateOptions());
            }
    

    对于Castle Windsor我们只需要像上面的UnitOfWorkInterceptor就是继承IInterceptor重写Intercept就可以实现动态代理啦。下面来看下自定义的UnitOfWorkAttribute。

        [AttributeUsage(AttributeTargets.Method)]
        public class UnitOfWorkAttribute : Attribute
        {
            /// <summary>
            /// Is this UOW transactional?
            /// Uses default value if not supplied.
            /// </summary>
            public bool? IsTransactional { get; private set; }
    
            /// <summary>
            /// Timeout of UOW As milliseconds.
            /// Uses default value if not supplied.
            /// </summary>
            public TimeSpan? Timeout { get; private set; }
    

    好了,定义了UnitOfWorkAttribute,那么我们怎么让它和UnitOfWorkInterceptor结合起来对代码进行动态拦截呢?

            [UnitOfWork]
            public virtual async Task<AbpLoginResult> LoginAsync(string userNameOrEmailAddress, string plainPassword, string tenancyName = null)
            {
                if (userNameOrEmailAddress.IsNullOrEmpty())
                {
                    throw new ArgumentNullException("userNameOrEmailAddress");
                }
    
                if (plainPassword.IsNullOrEmpty())
                {
                    throw new ArgumentNullException("plainPassword");
                }
    
                using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant))
                {
                    TUser user;
    
                    if (!_multiTenancyConfig.IsEnabled)
                    {
                        using (_unitOfWorkManager.Current.EnableFilter(AbpDataFilters.MayHaveTenant))
                        {
                            //Log in with default denant
                            user = await FindByNameOrEmailAsync(userNameOrEmailAddress);
                            if (user == null)
                            {
                                return new AbpLoginResult(AbpLoginResultType.InvalidUserNameOrEmailAddress);
                            }
                        }
                    }
    
    

    上面代码是利用Attribute的特性对方法进行标识,这是第一步,现在我们已经对要拦截的代码标识了,那么我们是怎么知道它要被拦截的呢?

        internal static class UnitOfWorkRegistrar
        {
            /// <summary>
            /// Initializes the registerer.
            /// </summary>sssss
            /// <param name="iocManager">IOC manager</param>
            public static void Initialize(IIocManager iocManager)
            {
                iocManager.IocContainer.Kernel.ComponentRegistered += ComponentRegistered;
            }
    
    
            /// <summary>
            /// 拦截注册事件 
            /// </summary>
            /// <param name="key"></param>
            /// <param name="handler"></param>
            private static void ComponentRegistered(string key, IHandler handler)
            {
                if (UnitOfWorkHelper.IsConventionalUowClass(handler.ComponentModel.Implementation))
                {
                    //判断如果是IRepository和IApplicationService,就注册动态代理 Intercept all methods of all repositories.
                    handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));
                }
                else if (handler.ComponentModel.Implementation.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any(UnitOfWorkHelper.HasUnitOfWorkAttribute))
                {
                    //判断如果是被标识了UnitOfWork attribute的就注册动态代理 Intercept all methods of classes those have at least one method that has UnitOfWork attribute.
                    //TODO: Intecept only UnitOfWork methods, not other methods!
                    handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));
                }
            }
        }
    

    请认真看上面的方法ComponentRegistered的代码,UnitOfWorkHelper.HasUnitOfWorkAttribute就是判断是否是UnitOfWorkAttribute。

            public static bool HasUnitOfWorkAttribute(MemberInfo methodInfo)
            {
                return methodInfo.IsDefined(typeof(UnitOfWorkAttribute), true);
            }
    

    你可能会问UnitOfWorkRegistrar的ComponentRegistered方法是什么时候执行的?那么你可以参考下我之前写的Castle Windsor常用介绍以及其在ABP项目的应用介绍 ,关于UnitOfWorkAttribute 是怎么执行的可以参考ABP之模块分析

    那么到此我们被标识[UnitOfWork]的登录方法LoginAsync和所有的repositories仓储一旦被执行到就会被拦截,执行我们的代理类。本来这篇文章还想说说仓储的,但是因为篇幅可能会有点长,那就放在下次总结吧,至此UnitOfWork也就总结到此了。

     

    参考文章:

    http://www.cnblogs.com/daxnet/archive/2011/06/03/2071931.html

    http://www.cnblogs.com/zhili/p/UnitOfWork.html

    http://www.cnblogs.com/xishuai/p/3750154.html

    转载于:https://www.cnblogs.com/huaizuo/p/4838680.html

    展开全文
  • 最近在研究DDD,同时也下载了一些基于DDD做的成熟案例用来学习,有一些吧,过于成熟,顺便就从里面取了取别的经,比如这个ByteartRetail项目,里面对数据的操作狠花了我一些时间 展开看看 ...

    最近在研究DDD,同时也下载了一些基于DDD做的成熟案例用来学习,有一些吧,过于成熟,顺便就从里面取了取别的经,比如这个ByteartRetail项目,里面对数据的操作狠花了我一些时间

    image

    展开看看

    image

    image

    其实有个问题很明显,同为基于DDD进行的项目架构设计,不同人设计的项目分层、命名和里面放的东西都不相同,看样子DDD这玩意,纯粹就是一个思维方式,所以也只能从各种实现里面找自己最能懂的来实现适合自己的架构方式了。很抱歉,因为找示例的时候找得太多,就忘了每个项目的来处了,不过大多是在StackOverflow里面别人推荐的,你们可以去找找看。

    上面这个项目它实现了用A.And(B).And(C).Or(D)这种方式来动态生成lambda表达式,这个不在本篇讨论范围内,看一看它是怎么用Repository模式和Unit Of Work模式来做数据层的

    image

    因为用的EF框架,所以我们直接看EntityFrameworkRepository,对外就是用的它的方法。

    这个图我简化了一些,但骨干都有了,首先,IRepository容器接口定义了基本的增删改查接口(复杂查询、分页都可以做在里面)

    其次,IUnitOfWork接口按unit of work思想把数据操作和提交分离,听起来很高深,但实现上,不过是在所有的数据操作里面不去SaveChanges(),而是通过标记状态的方式改变数据(由IRepositoryContext定义),最后统一由Commit方法来提交更改。出于各司其职的需要,所以把简单的事变成了三个接口,分别处理:增删改的对外接口、增删改的标记动作、事务的提交和回滚,这三件事

    于是,分别有了Repository和RepositoryContext这两个类,它们实现的哪个接口见上图

    接下来,IEntityFrameworkRepositoryContext接口主要是为了针对Entity Framework做实现,所以加了一个DbContext的属性,也仅仅只是加了一个属性。这样,EntityFrameworkRepositoryContext实现IEntityFrameworkRepositoryContext的同时,继承Repository,就拥有了对DbContext对象进行标识增删改和提交、回滚的能力了。

    到此,我们再把Repository类扩展一下,用EntityFrameworkRepository来继承之,从构造函数传入上面的包含了DbContext对象的EFcontext类(接口),把标识和提交的能力都带了进来,最终一个符合unit of work模式的具有操作DbContext对象的数据层就诞生了。

     

    思路搞清了,我就简化了一下,如下:

    image

    这一次,我把Repository变为了最终暴露的类。但是实现的时候碰到了一个难题。示例程序中,DbContext对象居然是直接在EFRepositoryContext内部直接new出来的:

    private readonly ThreadLocal<ByteartRetailDbContext> localCtx = new ThreadLocal<ByteartRetailDbContext>(() => new ByteartRetailDbContext());

    这样,上面的红字我标出了它把DbContext带进去的方式,是把dbcontext实例化了,然后整个对象变成构造函数传进去,我不行啊,这个DbContext很可能不止有一个啊,那么就只有把DbContext本身变成RepositoryContext的构造函数(而不是示例中的repositorycontext)了,然后Repository继承它。最终形成上图。

    image

    这种方式有一个缺点,就是最终的容器不是同其接口一一对应的,而是扩展了别的功能,结果就是在外部使用的时候必须用Repository实例,而不能用接口,以实现依赖注入(DI)的功能,管它呢,先告一段落吧

    image

    试用一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //测试服务接口
    public interface Itest
    {
        string outstirng(string msg);
    }
     
    //测试服务实现
    public string outstirng()
    {
        var u = new Repository<user>(new myEntities());//DbContext由此传入
        user us = u.GetEntity(m => m.id == 2);
        us.addr = "update" + DateTime.Now.ToString("ddfff");
        u.Update(us);
        us = u.GetEntity(m => m.id == 1);
        u.Delete(us);
        u.Commit();//一次提交,不提交不生效
        us = u.GetEntity(m => m.id == 2);
        return us.addr;
    }

    在MVC4项目里测试,用了SimpleInjector做IOC的注入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //注入
    public static void Initialize()
    {
        var container = new Container();
        InitializeContainer(container);
        container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
        container.RegisterMvcAttributeFilterProvider();
        container.Verify();
        DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));
    }
    private static void InitializeContainer(Container container)
    {
        container.Register<Itest, impltest>();
    }

    Controller:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class TestController : Controller
    {
        Itest svr;
        public TestController(Itest service)
        {
            svr = service;
        }
        public string Index()
        {
            return svr.outstirng();
        }
    }

    顺利得到结果

    转载于:https://www.cnblogs.com/zhangchenliang/archive/2012/12/16/2820962.html

    展开全文
  • .NET仓储模式高级用例

    千次阅读 2016-11-10 07:09:20
    主要结论 如果需要执行基本CURD之外的其他操作,此时就有必要使用仓储(Repository)。 为了促进测试工作并改善可靠性,应将仓储视作可重复使用的库(Library)。 将安全和审计功能放入仓储中可减少Bug并...

    主要结论

    • 如果需要执行基本CURD之外的其他操作,此时就有必要使用仓储(Repository)。

    • 为了促进测试工作并改善可靠性,应将仓储视作可重复使用的库(Library)。

    • 将安全和审计功能放入仓储中可减少Bug并简化应用程序。

    • 对ORM的选择不会限制仓储的用途,只会影响仓储承担的工作量。

    在之前发布的文章使用实体框架、Dapper和Chain的仓储模式实现策略中,我们介绍了实现仓储所需的基本模式。很多情况下,这些模式只是围绕底层数据访问技术,本质上并非完全必要的薄层。然而通过构建这样的仓储将获得很多新的机会。

    在设计仓储时,需要从“必须发生的事”这个角度来思考。例如,假设制订了一条规则,每当一条记录被更新后,其“LastModifiedBy”列必须设置为当前用户。但我们并不需要在每次保存前更新应用程序代码中的LastModifiedBy,可以直接将相关函数放在仓储中。

    通过将数据访问层视作管理所有“必须发生的事情”细节的独立库,即可大幅减少实现过程中的错误数量。与此同时可以简化基于仓储构建的代码,因为已经不再需要考虑“记账”之类的任务。

    注意:本文会尽量提供适用于实体框架(Entity Framework)Dapper和/或Tortuga Chain的代码范例,然而大部分仓储功能均可通过不依赖具体ORM的方式实现。

    审计列

    大部分应用程序最终需要追踪谁在什么时间更改了数据库。对于简单的数据库,这是通过审计列(Audit column)的形式实现的。虽然名称可能各不相同,但审计列通常主要承担下列四个角色:

    • 创建者的User Key

    • 创建日期/时间

    • 最后修改者的User Key

    • 最后修改日期/时间

    取决于应用程序的安全需求,可能还存在其他审计列,例如:

    • 删除者的User Key

    • 删除日期/时间

    • [创建 | 最后修改 | 删除] 者的Application Key

    • [创建 | 最后修改 | 删除] 者的IP地址

    从技术角度来看日期列很容易处理,但User Key的处理就需要费些功夫了,这里需要的是“可感知上下文的仓储”。

    常规的仓储是无法感知上下文的,这意味着除了连接数据库时绝对必要的信息,仓储无法获知其他任何信息。如果能正确地设计,仓储可以是彻底无状态(Stateless)的,这样即可在整个应用程序中共享一个实例。

    可感知上下文的仓储略微复杂。除非了解上下文,否则无法创建这种仓储,而上下文至少要包含当前活跃用户的ID和Key。对于某些应用程序这就够了,但对于其他应用程序,我们可能还需要传递整个用户对象和/或代表运行中应用程序的对象。

    Chain

    Chain通过一种名为审计规则(Audit rule)的功能为此提供了内建的支持。审计规则可供我们根据列名指定要覆盖(Override)的值。该功能包含了拆箱即用的基于日期的规则,以及从用户对象将属性复制到列的规则。范例:

    dataSource = dataSource.WithRules(
       new UserDataRule("CreatedByKey", "UserKey", OperationType.Insert),
       new UserDataRule("UpdatedByKey", "UserKey", OperationType.InsertOrUpdate),
       new DateTimeRule("CreatedDate", DateTimeKind.Local, OperationType.Insert),
    new DateTimeRule("UpdatedDate", DateTimeKind.Local, OperationType.InsertOrUpdate) );

    如上所述为了实现这一点我们需要一种可感知上下文的仓储。从下列构造函数中可以看到如何将上下文传递给不可变数据源,并使用必要信息新建数据源。

    public EmployeeRepository(DataSource dataSource, User user)
    {
        m_DataSource = dataSource.WithUser(user);
    }

    借此即可使用自行选择的DI框架针对每个请求自动创建并填写仓储。

    实体框架

    为了在实体框架中实现审计列的全局应用,我们需要利用ObjectStateManager并创建一个专用接口。该接口(如果愿意也可以称之为“基类(Base class)”)看起来类似这样:

    public interface IAuditableEntity 
    {
        DateTime CreatedDate {get; set;}
        DateTime UpdatedDate {get; set;}
        DateTime CreatedDate {get; set;}
        DateTime CreatedDate {get; set;}
    }

    随后该接口(或基类)会应用给数据库中与审计列匹配的每个实体。

    随后需要通过下列方式对DataContext类的Save方法进行覆盖(Override)。

    public override int SaveChanges()
    {
        // Get added entries
        IEnumerable<ObjectStateEntry> addedEntryCollection = Context
            .ObjectContext
            .ObjectStateManager
            .GetObjectStateEntries(EntityState.Added)
            .Where(m => m != null && m.Entity != null);
    
        // Get modified entries
        IEnumerable<ObjectStateEntry> modifiedEntryCollection = Context
            .ObjectContext
            .ObjectStateManager
            .GetObjectStateEntries(EntityState.Modified)
            .Where(m => m != null && m.Entity != null);
    
        // Set audit fields of added entries
        foreach (ObjectStateEntry entry in addedEntryCollection)
        {                
            var addedEntity = entry.Entity as IAuditableEntity;
            if (addedEntity != null)
            {
                addedEntity.CreatedDate = DateTime.Now;
                addedEntity.CreatedByKey = m_User.UserKey;
                addedEntity.UpdatedDate = DateTime.Now;
                addedEntity.UpdatedByKey = m_User.UserKey;
            }
    
        }
    
        // Set audit fields of modified entries
        foreach (ObjectStateEntry entry in modifiedEntryCollection)
        {
            var modifiedEntity = entry.Entity as IAuditableEntity;
            if (modifiedEntity != null)
            {
                modifiedEntity.UpdatedDate = DateTime.Now;
                modifiedEntity.UpdatedByKey = m_User.UserKey;
            }
        }
        return SaveChanges();
    }

    如果需要大量使用实体框架(EF),则有必要非常熟悉ObjectStateManager及其能力。因为有关进行中事务的大部分有用元数据都包含在ObjectStateManager中。

    最后还需要修改数据上下文(可能还有仓储)的构造函数以使其接受用户对象。

    虽然看似这要编写大量代码,但每个EF数据上下文只需要编写一次。与上文的范例类似,数据上下文和仓储的实际创建工作可由DI框架负责进行。

    历史表

    很多地方性的法规和制度要求对记录的改动进行追踪,此外这样做也可以简化诊断工作。

    对此的常规建议是直接让数据库自行处理。一些数据库内建包含了类似的功能,这类功能通常叫做时间表(Temporal table)。其他数据库则可使用触发器模拟出类似的功能。无论哪种情况,应用程序都不会发现额外的日志操作,因此这种技术出错的概率也得以大幅降低。

    如果出于某些原因无法使用时间表或触发器,那么仓储需要能明确写入历史表。

    无论将维持历史表的代码放在哪里,都有两个基本惯例需要遵循。一致性在这里真的很重要,如果一些表遵循一个惯例,其他表遵循另一个管理,最终只能造成混乱。

    写入前复制:在这个惯例中,需要在实际执行更新或删除操作前将老的记录从活动(Live)表复制到历史表。这意味着历史表绝对不会包含当前记录。因此需要将活动表和历史表联接在一起才能看到完整的变更历史。

    复制前写入:或者可以首先更新活动表,随后将该行复制到历史表。这种做法的优势在于历史表中包含完整的记录,无需进行上文提到的联接。不足之处在于,由于这种做法需要复制数据,因此会耗费更多空间。

    无论哪种惯例,都可以使用软删除了解是谁实际删除了行。如果需要使用硬删除,也只能在执行软删除之后再进行硬删除。

    软删除

    使用仓储可获得的另一个优势在于可以在应用程序无法察觉的情况下从硬删除切换为软删除。软删除可用能被应用程序察觉的方式删除记录,但删除的记录可继续保留在数据库中,以便用于审计等用途。此外在必要时应用程序还可以恢复被软删除的记录。

    为避免数据丢失,不应针对为软删除提供支持的表为应用程序分配DELETE特权。如果应用程序无意中试图执行硬删除,权限检查功能会显示错误信息,而不会直接删除行。

    Chain

    Chain通过自己的审计规则基础架构提供了隐式的软删除支持。在配置软删除规则后,按照习惯还需要配置匹配审计(Matching audit)列:

    var dataSource = dataSource.WithRules(
        new SoftDeleteRule("DeletedFlag", true, OperationTypes.SelectOrDelete),
        new UserDataRule("DeletedByKey", "EmployeeKey", OperationTypes.Delete),
        new DateTimeRule("DeletedDate", DateTimeKind.Local, OperationTypes.Delete)
        );

    在发现表包含软删除列(例如本例中的DeletedFlag)后,会自动发生两件事:

    • 所有查询的WHERE子句可暗中添加“AND DeletedFlag = 0”。

    • 所有对DataSource.Delete的调用将变成更新(Update)语句以设置 deleted flag。

    实体框架

    在实体框架中,可以在读取为软删除提供支持的表的每个查询中包含一个额外的Where子句。此外还需要将任何删除操作手工转换为更新操作,使用对象图(Object graph)时这一点可能较难办到。

    另一种方法的过程较繁琐,但可能更不易出错。首先在DataContext.OnModelCreating覆盖中明确列出每个支持软删除的表。

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
       modelBuilder.Entity<Employee>().
    Map(m => m.Requires("IsDeleted").HasValue(false)); }

    随后需要覆盖Save方法以确保删除操作可变成更新操作。Stackoverflow上的Colin提供了这种模式。

    public override int SaveChanges()
    {
       foreach (var entry in ChangeTracker.Entries()
                 .Where(p => p.State == EntityState.Deleted 
                 && p.Entity is ModelBase))
        SoftDelete(entry);
        return base.SaveChanges();
    }
    
    private void SoftDelete(DbEntityEntry entry)
    {
        var e = (ModelBase)entry.Entity;
        string tableName = GetTableName(e.GetType());
        Database.ExecuteSqlCommand(
                 String.Format("UPDATE {0} SET IsDeleted = 1 WHERE ID = @id", tableName)
                 , new SqlParameter("id", e.ID));
    
        //Marking it Detached prevents the hard delete
        entry.State = EntityState.Detached;
    }

    建议阅读Colin回答中的剩余内容,这些回答解决了很多边界案例问题。

    访问日志记录

    虽然审计列、历史表,以及软删除均适用于写入操作场景,但有时候可能还要用日志记录读取操作。例如美国医疗健康行业中,医护人员需要能够在紧急情况下访问病患的医疗记录。但在正常业务中他们只有在为病患提供治疗的过程中可以合法访问这些记录。

    由于记录不能彻底锁定,因此作为权宜之计只能追踪读取过每条记录的人的身份。在仓储层面上,只需要对每个涉及敏感数据的查询进行日志记录即可轻松实现。最简单的方法是在相关仓储方法的基础上手工实现。

    性能日志

    用户体验已成为一项功能,因此我们有必要了解每个查询到底要花费多长时间。单纯追踪每页面性能还不够,因为一个页面可能涉及多个查询。对于实体框架这一点尤为重要,因为延迟加载(Lazy-loading)可能会将数据库调用隐藏起来。

    仓储中的显式日志记录

    虽然很枯燥并且很容易漏掉某个查询,但可将每个查询封装到“即抛型”计时器中。具体模式如下:

    public class OperationTimer : IDisposable
    {
        readonly object m_Context;
        readonly Stopwatch m_Timer;
    
        public OperationTimer(object context)
        {
            m_Context = context;
            m_Timer = Stopwatch.StartNew();
        }
        public void Dispose()
        {
            //Write to log here using timer and context
        }
    }

    具体用法为:

    using(new OperationTimer("Load employees"))
    {
        //execute query here
    } 
    Chain

    Chain在数据源层面上暴露了一系列事件。本例需要的是DataSource.ExecutionFinished。范例如下:

    static void DefaultDispatcher_ExecutionFinished(object sender, ExecutionEventArgs e)
    {
        Debug.WriteLine($"Execution finished: {e.ExecutionDetails.OperationName}. Duration: {e.Duration.Value.TotalSeconds.ToString("N3")} sec. Rows affected: {(e.RowsAffected != null ? e.RowsAffected.Value.ToString("N0") : "<NULL>")}.");
    }

    此外还可将句柄附加到DataSource.GlobalExecutionFinished,借此侦听来自所有数据源的事件。

    实体框架

    实体框架内建的日志能力无法衡量每个查询所需的时间。为了消除这种局限,我们可以使用自定义的IDbCommandInterceptor

    public class EFLoggerForTesting : IDbCommandInterceptor
    {
        static readonly ConcurrentDictionary<DbCommand, DateTime> m_StartTime = new ConcurrentDictionary<DbCommand, DateTime>();
    
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            Log(command, interceptionContext);
        }
    
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            Log(command, interceptionContext);
        }
    
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            Log(command, interceptionContext);
        }
    
        private static void Log<T>(DbCommand command, DbCommandInterceptionContext<T> interceptionContext)
        {
            DateTime startTime;
            TimeSpan duration;
    
            m_StartTime.TryRemove(command, out startTime);
            if (startTime != default(DateTime))
            {
                duration = DateTime.Now - startTime;
            }
    else
                duration = TimeSpan.Zero;
    
            string message;
    
            var parameters = new StringBuilder();
            foreach (DbParameter param in command.Parameters)
            {
                parameters.AppendLine(param.ParameterName + " " + param.DbType + " = " + param.Value);
            }
    
            if (interceptionContext.Exception == null)
            {
                message = string.Format("Database call took {0} sec. RequestId {1} \r\nCommand:\r\n{2}", duration.TotalSeconds.ToString("N3"), requestId, parameters.ToString() + command.CommandText);
            }
            else
            {
                message = string.Format("EF Database call failed after {0} sec. RequestId {1} \r\nCommand:\r\n{2}\r\nError:{3} ", duration.TotalSeconds.ToString("N3"), requestId, parameters.ToString() + command.CommandText, interceptionContext.Exception);
            }
    
            Debug.WriteLine(message);
        }
    
    
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            OnStart(command);
        }
    
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            OnStart(command);
        }
    
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            OnStart(command);
        }
    
        private static void OnStart(DbCommand command)
        {
            m_StartTime.TryAdd(command, DateTime.Now);
        }
    }

    虽然这种方式无法获取上下文数据,但可酌情将上下文推出(Shove)至ThreadLocalAsyncLocal以绕过这一局限。

    权限检查 – 表级

    虽然可执行应用程序级别的权限检查,但同时强制进行仓储级的检查也能提供一定的好处。这种做法可以避免忘了对新创建的Screen/页面进行权限检查。

    仓储强制执行

    实现这一切最简单的方法是在每个相关函数开始时执行角色检查。例如:

    public int Insert(Employee employee)
            {
                if (!m_User.IsAdmin)
                    throw new SecurityException("Only admins may add employees");
    数据库强制执行

    更成熟的做法是创建多个连接字符串。在创建仓储时,可根据用户角色选择连接字符串。在本例中非管理员用户的连接字符串针对employee表不具备INSERT特权。

    由于复杂度和繁琐的维护,除非需要多层防御机制,对安全性要求极高的环境,否则不建议使用这种方法。就算在这种情况下,也需要通过大量的自动化测试确保每个连接字符串只包含自己需要的全部权限。

    权限检查 – 列级

    有时候可能需要进行列级的权限检查。例如我们可能需要防止用户为自己分配管理员特权,或可能希望阻止经理之外的其他用户查看员工的薪资数据。

    Chain

    Chain可以利用自带的审计规则功能实现列级权限检查。此时会将匿名函数与列名称,以及受限制操作列表一起传递至RestrictColumn构造函数。(并可选指定表名称。)

    var IsAdminCheck = user => ((User)user).IsAdmin;
    
    dataSource = dataSource.WithRules(
        new RestrictColumn("Users", "IsAdmin", 
    OperationTypes.Insert|OperationTypes.Update, IsAdminCheck));

    为防止读取受限制的列,可将其传递至OperationTypes.Select flag

    Dapper

    在Dapper中实现这一目标的最简单方法是使用多个SQL语句。如果用户缺乏某一特权,只需要选择忽略对应列的SQL语句即可。

    实体框架

    查询可使用下列几个选项:

    1. 根据用户角色手工创建不同的投影(例如Select子句)。

    2. 正常执行查询,随后如果权限检查失败,对结果集进行循环,并将受限制的属性设置为null/0。

    对于插入,按照上述方法将受限制属性留空即可。

    更新操作较为复杂。当写入特定列的操作受限时将无法附加实体。此时需要重新获取原始记录,对允许的值进行复制并保存该对象,而不要保存应用程序代码传递来的对象。(基本上这就是上一篇文章提到的“新手”模式。)

    将一个模型映射至多个表

    数据架构方面有一个很重要的概念,即:无需在表和类之间创建一对一映射。为了让数据库的运转更高效或满足特定业务规则的需求,通常可能需要将一个类映射至多个表。

    假设需要记录有关棒球队的数据,可能会用到这些表:

    主键

    Team

    Team

    TeamSeasonMap

    TeamKey+SeasonKey

    如果应用程序只能在有关赛季(Season)的上下文中理解团队(Team)的概念,那么可以用一个Team对象涵盖所有表。

    Chain

    Chain中的类和表之间不具备强关系,这意味着对于更新操作,应该这样写代码:

    dataSource.Update("Team", myTeam).Execute();
    dataSource.Update("TeamSeasonMap", myTeam).Execute();

    代码运行时会判断哪些表适用哪些属性,并酌情生成SQL语句。

    通过这种方式,即可从所有表的联接视图中获取Team对象。(Chain不支持直接联接,假设始终通过视图实现。)

    实体框架

    实体框架会认为映射至同一实体的多个表严格共享相同的主键。这意味着将无法支持该场景。

    • 对于读取操作,可以使用EF的常规LINQ语法执行联接和投影。

    • 对于更新操作,需要将每个表的模型复制到单独的实体中。

    缓存

    一般来说,仓储都需要考虑缓存问题。由于仓储知道数据的修改时间,因此可充当处理缓存失效问题的最佳方法。

    Chain

    Chain支持缓存,但必须通过Appender分别应用给每个查询。Appender可附加至实际执行之前的操作中,在本例中我们需要关注四个Appender:

    • .Cache(...)

    • .CacheAllItems(...)

    • .InvalidateCache(...)

    • .ReadOrCache(...)

    也许通过仓储范例可以更好地说明这些Appender的作用。在下面的例子中可以看到对特定记录创建缓存,以及使用CacheAllItems对集合创建缓存这两种做法之间的相互作用。

    public class EmployeeCachingRepository
    {
    
        private const string TableName = "HR.Employee";
        private const string AllCacheKey = "HR.Employee ALL";
    
        public IClass1DataSource Source { get; private set; }
        public CachePolicy Policy { get; private set; }
    
        public EmployeeCachingRepository(IClass1DataSource source
    , CachePolicy policy = null) { Source = source; Policy = policy; } protected string CacheKey(int id) { return $"HR.Employee EmployeeKey={id}"; } protected string CacheKey(Employee entity) { return CacheKey(entity.EmployeeKey.Value); } public Employee Get(int id) { return Source.GetByKey(TableName, id)
    .ToObject<Employee>()
    .ReadOrCache(CacheKey(id), policy: Policy).Execute(); } public IList<Employee> GetAll() { return Source.From(TableName)
    .ToCollection<Employee>().CacheAllItems((Employee x) => CacheKey(x),
    policy: Policy).ReadOrCache(AllCacheKey, policy: Policy).Execute(); } public Employee Insert(Employee entity) { return Source.Insert(TableName, entity)
    .ToObject<Employee>()
    .InvalidateCache(AllCacheKey)
    .Cache((Employee x) => CacheKey(x), policy: Policy)
    .Execute(); } public Employee Update(Employee entity) { return Source.Update(TableName, entity)
    .ToObject<Employee>()
    .Cache(CacheKey(entity))
    .InvalidateCache(AllCacheKey).Execute(); } public void Delete(int id) { Source.DeleteByKey(TableName, id).
    InvalidateCache(CacheKey(id))
    .InvalidateCache(AllCacheKey).Execute(); } }

    从这个例子中可以发现,Chain为失效逻辑提供了丰富的控制能力,但作为代价我们必须慎重地指定各种选项。

    实体框架

    实体框架提供了两种级别的缓存。第一级仅限数据上下文,主要可用于确保对象图不包含代表同一条物理数据库记录的重复实体。由于该缓存会与数据上下文一起销毁,因此大部分缓存场景并不使用这种缓存。

    在EF的术语中,我们需要的是名为“二级缓存”的缓存。虽然该功能已包含在EF 5中,但第6版实体框架并未提供任何拆箱即用的缓存功能。因此我们需要使用第三方库,例如EntityFramework.CacheEFSecondLevelCache。从列举的这些库可以知道,为EF增加二级缓存并没有什么标准的模式。

    关于本文作者

    640?wx_fmt=jpegJonathan Allen的第一份工作是在九十年代末期为一家诊所开发MIS项目,借此帮助这家诊所逐渐由Access和Excel转向真正的企业级解决方案。在用五年时间为财政部开发自动化交易系统后,他开始担任各种项目的顾问,并从事了仓储机器人UI、癌症研究软件中间层,以及大型房地产保险企业大数据需求等各类项目。闲暇时他喜欢研究并撰文介绍16世纪的格斗术。

    原文地址:http://www.infoq.com/cn/articles/repository-advanced


    .NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

    640?wx_fmt=jpeg

    展开全文
  • <template> <el-table :data="list" border fit highlight-current-row style="width: 100%"> <el-table-column v-loading="loading" align="center" label="ID" ... element-loading-text="请给我...
    <template>
      <el-table :data="list" border fit highlight-current-row style="width: 100%">
        <el-table-column
          v-loading="loading"
          align="center"
          label="ID"
          width="65"
          element-loading-text="请给我点时间!"
        >
          <template slot-scope="scope">
            <span>{{ scope.row.id }}</span>
          </template>
        </el-table-column>
    
        <el-table-column width="180px" align="center" label="Date">
          <template slot-scope="scope">
            <span>{{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>
          </template>
        </el-table-column>
    
        <el-table-column min-width="300px" label="Title">
          <template slot-scope="{row}">
            <span>{{ row.title }}</span>
            <el-tag>{{ row.type }}</el-tag>
          </template>
        </el-table-column>
    
        <el-table-column width="110px" align="center" label="Author">
          <template slot-scope="scope">
            <span>{{ scope.row.author }}</span>
          </template>
        </el-table-column>
    
        <el-table-column width="120px" label="Importance">
          <template slot-scope="scope">
            <svg-icon v-for="n in +scope.row.importance" :key="n" icon-class="star" />
          </template>
        </el-table-column>
    
        <el-table-column align="center" label="Readings" width="95">
          <template slot-scope="scope">
            <span>{{ scope.row.pageviews }}</span>
          </template>
        </el-table-column>
    
        <el-table-column class-name="status-col" label="Status" width="110">
          <template slot-scope="{row}">
            <el-tag :type="row.status | statusFilter">
              {{ row.status }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </template>
    
    <script>
    import { fetchList } from '@/api/article'
    
    export default {
      filters: {
        statusFilter(status) {
          const statusMap = {
            published: 'success',
            draft: 'info',
            deleted: 'danger'
          }
          return statusMap[status]
        }
      },
      props: {
        type: {
          type: String,
          default: 'CN'
        }
      },
      data() {
        return {
          list: null,
          listQuery: {
            page: 1,
            limit: 5,
            type: this.type,
            sort: '+id'
          },
          loading: false
        }
      },
      created() {
        this.getList()
      },
      methods: {
        getList() {
          this.loading = true
          this.$emit('create') // for test
          fetchList(this.listQuery).then(response => {
            this.list = response.data.items
            this.loading = false
          })
        }
      }
    }
    </script>
    
    <template>
      <div class="tab-container">
        <el-tag>mounted times :{{ createdTimes }}</el-tag>
        <el-alert :closable="false" style="width:200px;display:inline-block;vertical-align: middle;margin-left:30px;" title="Tab with keep-alive" type="success" />
        <el-tabs v-model="activeName" style="margin-top:15px;" type="border-card">
          <el-tab-pane v-for="item in tabMapOptions" :key="item.key" :label="item.label" :name="item.key">
            <keep-alive>
              <tab-pane v-if="activeName==item.key" :type="item.key" @create="showCreatedTimes" />
            </keep-alive>
          </el-tab-pane>
        </el-tabs>
      </div>
    </template>
    
    <script>
    import TabPane from './components/TabPane'
    
    export default {
      name: 'Tab',
      components: { TabPane },
      data() {
        return {
          tabMapOptions: [
            { label: 'China', key: 'CN' },
            { label: 'USA', key: 'US' },
            { label: 'Japan', key: 'JP' },
            { label: 'Eurozone', key: 'EU' }
          ],
          activeName: 'CN',
          createdTimes: 0
        }
      },
      watch: {
        activeName(val) {
          this.$router.push(`${this.$route.path}?tab=${val}`)
        }
      },
      created() {
        // init the default selected tab
        const tab = this.$route.query.tab
        if (tab) {
          this.activeName = tab
        }
      },
      methods: {
        showCreatedTimes() {
          this.createdTimes = this.createdTimes + 1
        }
      }
    }
    </script>
    
    <style scoped>
      .tab-container {
        margin: 30px;
      }
    </style>
    
    展开全文
  • vue项目的前期配置 封装axios实例 封装API
  • Altium Designer 20 超全终极封装

    万次阅读 多人点赞 2019-08-08 16:10:25
    Altium Designer超全终极封装库(带3D封装) 很多网友老是找不到合适的封装,自己又懒得做。今天给大家链接,整理了大概4g左右的封装库,共各位网友学习下载。 文末附带下载链接! 下面是部分封装展示: 效果...
  • 封装ajax请求

    万次阅读 2020-06-08 02:24:05
    对ajax请求进行封装,希望通过ajax直接返回response.data数据 /* ajax请求函数模块 返回值: promise对象(异步返回的数据是: response.data) */ import axios from 'axios' export default function ajax (url, data...
  • js封装网易云api请求

    万次阅读 2020-06-11 18:41:45
    封装axios,以及封装网易云音乐api文档中的所有请求 封装号的js文件地址Github:云博前端项目-网易云音乐api封装后的js文件地址 向将axios请求封装 npm install axios -s 封装的请求函数文件,通过传入type=GET/POST...
  • PCB封装库——SOP封装

    千次下载 热门讨论 2013-04-23 15:33:13
    比较全的SOP和SSOP,protel的PCB库
  • 用EasySysprep封装Win7系统

    万次阅读 多人点赞 2018-01-05 02:23:28
    前面我介绍了使用Dism++封装Windows 7系统,不过最后还是没达到我想要的效果。不过经过一番查阅之后,我发现我想要的效果好像没办法简单的使用单一工具达到。我希望做成的效果是类似于Win10,一个ISO镜像内置各种...
  • 基于Glide的二次封装

    万次阅读 多人点赞 2019-02-16 09:29:34
    Glide二次封装库源码 前言 为什么选择Glide? Glide 轻量级 速度快 可以根据所需加载图片的大小自动适配所需分辨率的图 支持多种格式图片(静态webp,动态gif,jpeg,jpg,png) 支持多种数据源图片...
  • java提高篇(一)-----理解java的三大特性之封装

    万次阅读 多人点赞 2013-10-15 22:25:10
    从大二接触java开始,到现在也差不多三个年头了。...虽然是半道出家但是经过自己的努力也算是完成了“学业”。期间参加过培训机构,但是极其不喜欢那种培训方式,于是毅然的放弃了选择自学(可怜我出了6000块钱啊),虽然...
  • Vue封装组件的过程

    万次阅读 多人点赞 2018-03-20 09:41:05
    ● 组件可以扩展HTML元素,封装可重用代码 ● 在较高层面上,组件是自定义元素,Vue.js的编译器为他添加特殊功能 ● 有些情况下,组件也可以表现用 is 特性进行了扩展的原生的HTML元素 ● 所有的Vue组件同时也都...
  • Java 封装

    千次阅读 2018-12-24 13:25:29
    封装 java中的封装是将代码和数据一起封装到单个单元中的过程,例如胶囊,即几种药物的混合。 我们可以通过将类的所有数据成员设为private,以在java中创建一个完全封装的类。现在我们可以使用setter和getter方法来...
  • 本来想等S大的ES5出来顺便将本系列做成ES5的封装教程的,无奈软件迟迟没出来,而大家的学习心切,于是只好先用ES4.5把教程完结,以后出ES5再考虑出新教程。那么,鄙人想要早点用上ES5,相信你们也希望早点用上ES5,...
  • WIN7封装教程2018系列(六)—封装

    千次阅读 2018-11-27 10:31:35
    第六系列是整个教程的重点,步骤必须细致谨慎考虑周全,尽量不要出错,我也反复封装了两三次才成功,并不是你们在教程里看到的一次就成功。我直接把正确封装步骤的截图,按顺序编辑好了,照着我的教程做就行了。封装...
  • python封装成exe

    万次阅读 多人点赞 2018-10-17 14:45:15
    最近写了一个小小的程序,需要进行封装exe,为了简单,就直接用了pyinstaller这个模块,对于python3.6版本的童鞋来说,简直方便的不要。下面就给大家介绍一下如何用pyinstaller去封装程序为exe程序。 首先,需要安装...
  • TSSOP封装

    千次下载 热门讨论 2014-03-09 16:39:29
    所有TSSOP封装都在这了,画电路板必备。
  • WIN7封装教程+封装工具包

    热门讨论 2012-04-03 16:30:55
    WIN7封装教程+封装工具包(非常详细)
  • 0805封装尺寸/0402封装尺寸/0603封装尺寸/1206封装尺寸 封装尺寸与功率关系:  0201 1/20W  0402 1/16W  0603 1/10W  0805 1/8W  1206 1/4W 封装尺寸与封装的对应关系  0402=1.0mmx0.5mm ...
  • 一、使用VMwareworkstation创建虚拟机 虚拟机配置 1、删除声卡、打印机,硬盘设置100G,添加第二光驱,第一光驱挂载天空pe,第二光驱挂载系统镜像。 usb控制器配置3.0,这样在虚拟机里面鼠标控制会比较流畅。...
  • 电池CR1220的pcb封装

    热门讨论 2012-06-08 09:10:01
    电池座封装,cr1220,protel 99的也有 protel dxp的都有,电池座封装,cr1220,cr2032,cr2450
  • 电容尺寸、封装及PCB库

    万次阅读 多人点赞 2018-08-28 16:28:51
    无极性电容封装以RAD为标识,有RAD-0.1 RAD-0.2 RAD-0.3等,后面的娥数字代表焊盘中心孔间距,单位英寸 1in=1000mil 0.1in =100mil=0.254mm 常见的瓷片电容(104)其封装就是RAD-0.1,其表示为无极性电容,两引脚...
  • vue项目使用axios封装请求或者其他类型请求封装

    万次阅读 多人点赞 2019-09-22 16:37:37
    先看一个开源项目的写法,就非常符合需求了 import axios from 'axios' import { Message, MessageBox } from 'element-ui' import store from '@/store' import { getToken } from '@/utils/auth' ...
  • 【vue组件封装】ECharts组件封装随笔

    千次阅读 2020-09-05 20:18:11
    内容 记录组件封装思路 实现ECharts 组件的封装
  • Struts2数据封装

    千次阅读 2016-11-24 18:22:24
     Struts2提供了一些基于拦截器的数据封装方式,一共有四种,分为静态参数封装和动态属性封装,动态属性封装又可分为属性驱动和模型驱动,属性驱动又可分为基本属性驱动和对象图导航语言(OGNL,Object Graph ...
  • ** 本教程共有7个系列 ** 1. 母盘定制 2. 准备封装环境 ...如果有更新系统的需求的话,联网的时候就可以顺便更新一下系统,但是更新系统并不是必须的,这次封装的系统版本比较新,没必要更新的补丁,...
  • 封装和信息隐藏

    千次阅读 2018-11-08 19:45:24
    文章目录3.1 信息隐藏原则3.1.1 封装与信息隐藏3.1.2 接口扮演的角色3.2 创建对象的基本模式3.2.1 门户大开型对象3.2.2 用命名规范区别私用成员3.2.3 作用域、嵌套函数和闭包3.2.4 用闭包实现私用成员3.3 更多高级...
  • PCB封装库命名规则

    千次阅读 2014-08-02 06:45:09
    用DIP-引脚数量+尾缀来表示双列直插封装 尾缀有N和W两种,用来表示器件的体宽 N为体窄的封装,体宽300mil,引脚间距2.54mm W为体宽的封装, 体宽600mil,引脚间距2.54mm 如:DIP-16N表示的是体宽300mil,引脚间距2.54...
  • 以太网封装及vlan封装类型

    千次阅读 2013-04-18 16:35:12
    以下是各种封装:   以太网II封装: 以太网技术的基础是以太网帧,也作标准以太网帧,也称为ARPA,即以太网II帧(最初的以太网II标准也称为DIX,由Digital,Intel和Xerox三家发起公司的首字母拼合而成)。帧格式...

空空如也

1 2 3 4 5 ... 20
收藏数 2,052,473
精华内容 820,989
关键字:

unitofwork高级封装