zoukankan      html  css  js  c++  java
  • [译]ABP框架使用AngularJs,ASP.NET MVC,Web API和EntityFramework构建N层架构的SPA应用程序

    本文演示ABP框架如何使用AngularJs,ASP.NET MVC,Web API 和EntityFramework构建基于N层架构的多语言SPA应用程序

    Simple Task System Screenshot
    演示程序截图如上所示.

    内容摘要

    介绍

    在这篇文章, 我将基于以下框架演示如何开发单页面的(SPA) 应用程序 :

    • ASP.NET MVC 和 ASP.NET Web API  web站点的基础框架.
    • Angularjs  SPA 框架.
    • EntityFramework  ORM (Object-Relational Mapping) 框架
    • Castle Windsor – 依赖注入框架.
    • Twitter Bootstrap – 前端框架.
    • Log4Net 来记录日志, AutoMapper 实体对象映射.
    • 和 ASP.NET Boilerplate 作为应用程序模板框架.

    ASP.NET Boilerplate [1] 是一个开源的应用程序框架,它包含了一些常用的组件让您能够快速简单的开发应用. 它集成一些常用的基础框架. 比如依赖注入领域驱动设计 和分层架构. 本应用演示ABP如何实现验证,异常处理,本地化响应式设计.

    使用 boilerplate 模板创建程序

    ASP.NET Boilerplate给我们提供了一个非常好的构建企业应用的模板,以便节约我们构建应用程序的时间。
    在www.aspnetboilerplate.com/Templates目录,我们可以使用模板创建应用。

    Create template by ASP.NET Boilerplate

    这里我选择 SPA(单页面程序)使用AngularJs EntityFramework框架. 然后输入项目名称SimpleTaskSystem.来创建和下载应用模版.下载的模版解决方案包含5个项目. Core 项目是领域 (业务) 层, Application 项目是应用层, WebApi 项目实现了 Web Api 控制器, Web 项目是展示层,最后EntityFramework 项目实现了EntityFramework框架.

    Note: 如果您下载本文演示实例, 你会看到解决方案有7个项目. 我把NHibernate和Durandal都放到本演示中.如果你对NHibernate,Durandal不感兴趣的话,可以忽略它们.

    创建实体对象

    我将创建一个简单的应用程序来演示任务和分配任务的人. 所以我需要创建Task实体对象和Person实体对象.

    Task实体对象简单的定义了一些描述:CreationTime和Task的状态. 它同样包含了Person(AssignedPerson)的关联引用:

    public class Task : Entity<long>
    {
        [ForeignKey("AssignedPersonId")]
        public virtual Person AssignedPerson{ get; set; }
        public virtual int? AssignedPersonId { get; set; }
        public virtual string Description { get; set; }
        public virtual DateTime CreationTime { get; set; }
        public virtual TaskState State { get; set; }  public Task()
        {
            CreationTime = DateTime.Now;
            State = TaskState.Active;
        }
    }
    
    

    Person实体对象简单的定义下Name:

    public class Person : Entity
    {
        public virtual string Name { get; set; }
    }

    ASP.NET Boilerplate 给 Entity 类定义了 Id 属性. 从Entity 类派生实体对象将继承Id属性. Task 类从 Entity<long>派生将包含 long 类型的ID. Person 类包含 int 类型的ID. 因为int是默认的主键类型, 这里我不需要特殊指定.

    我在这个 Core 项目里添加实体对象因为实体对象是属于领域/业务层的.

    创建 DbContext

    众所周知, EntityFramework使用DbContext 类工作. 我们首先得定义它. ASP.NET Boilerplate创建了一个DbContext模板给我们. 我们只需要添加 IDbSets给 Task and Person. 完整的 DbContext 类如下:

    public class SimpleTaskSystemDbContext : AbpDbContext
    {
        public virtual IDbSet<Task> Tasks { get; set; }
        public virtual IDbSet<Person> People { get; set; }
        public SimpleTaskSystemDbContext(): base("Default")
        {
    
        }
        public SimpleTaskSystemDbContext(string nameOrConnectionString): base(nameOrConnectionString)
        {
                
        }
    }

    我们还需要在web.config添加默认的连接字符串. 如下:

    <add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystem; Trusted_Connection=True;" providerName="System.Data.SqlClient" />

    创建数据库迁移

    我们将使用EntityFramework的Code First模式来迁移和创建数据库. ASP.NET Boilerplate模板默认支持签约但需要我们添加如下的Configuration 类:

    internalinternal sealed class Configuration : DbMigrationsConfiguration<SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }
        protected override void Seed(SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext context)
        {
            context.People.AddOrUpdate(
                p => p.Name, new Person {Name = "Isaac Asimov"}, new Person {Name = "Thomas More"}, new Person {Name = "George Orwell"}, new Person {Name = "Douglas Adams"}
                );
        }
    }

    另一种方法, 是在初始化的是添加4个Person. 我将创建初始迁移.打开包管理控台程序并输入以下命令:

    Visual studio Package manager console

    Add-Migration “InitialCreate” 命令创建 InitialCreate 类如下:

    public partial class InitialCreate : DbMigration
    {
        public override void Up()
        {
            CreateTable( "dbo.StsPeople",
                c => new {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(),
                    })
                .PrimaryKey(t => t.Id);
                
            CreateTable( "dbo.StsTasks",
                c => new {
                        Id = c.Long(nullable: false, identity: true),
                        AssignedPersonId = c.Int(),
                        Description = c.String(),
                        CreationTime = c.DateTime(nullable: false),
                        State = c.Byte(nullable: false),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.StsPeople", t => t.AssignedPersonId)
                .Index(t => t.AssignedPersonId);            
        }
    
        public override void Down()
        {
            DropForeignKey("dbo.StsTasks", "AssignedPersonId", "dbo.StsPeople");
            DropIndex("dbo.StsTasks", new[] { "AssignedPersonId" });
            DropTable("dbo.StsTasks");
            DropTable("dbo.StsPeople");
        }
    }

    我们已经创建了数据库类, 但是还没创建那数据库. 下面来创建数据库,命令如下:

    PM> Update-Database

    这个命令帮我们创建好了数据库并填充了初始数据:

    Database created by EntityFramework Migrations

    当我们修改实体类时, 我们可以通过 Add-Migration 命令很容易的创建迁移类。需要更新数据库的时候则可以通过 Update-Database 命令. 关于更多的数据库迁移, 可以查看 entity framework的官方文档.

    定义库

    在领域驱动设计中, repositories 用于实现特定的代码. ASP.NET Boilerplate 使用 IRepository 接口给每个实体自动的创建 repository . IRepository 定义了常用的方法如 select, insert, update, delete 等,更多如下:

    IRepository interface

    我们还可以根据我们的需要来扩展 repository . 如果需要单独的实现接口的话,首先需要继承 repositories 接口. Task repository 接口如下:

    public interface ITaskRepository : IRepository<Task, long>
    {
        List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
    }

    这继承了ASP.NET Boilerplate 的 IRepository 接口. ITaskRepository 默认定义了这些方法. 我们也可以添加自己的方法 GetAllWithPeople(…).

    这里不需要再为Person创建 repository 了,因为默认的方法已经足够. ASP.NET Boilerplate 提供了通用的 repositories 而不需要创建 repository 类. 在’构建应用程序服务层’ 章节中的TaskAppService 类中将演示这些..

    repository 接口被定义在Core 项目中因为它们是属于领域/业务层的.

    实现库

    我们需要实现上述的 ITaskRepository 接口. 我们在EntityFramework 项目实现 repositories. 因此,领域层完全独立于 EntityFramework.

    当我们创建项目模板, ASP.NET Boilerplate 在项目中为 repositories 定义了一些基本类: SimpleTaskSystemRepositoryBase. 这是一个非常好的方式来添加基本类,因为我们可以为repositories稍后添加方法. 你可以看下面代码定义的这个类.定义TaskRepository 从它派生:

    public class TaskRepository : SimpleTaskSystemRepositoryBase<Task, long>, ITaskRepository
    {
        public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state)
        {
        //In repository methods, we do not deal with create/dispose DB connections, DbContexes and transactions. ABP handles it.
        var query = GetAll();
        //GetAll() returns IQueryable<T>, so we can query over it.
        //var query = Context.Tasks.AsQueryable(); 
        //Alternatively, we can directly use EF's DbContext object.
        //var query = Table.AsQueryable(); 
        //Another alternative: We can directly use 'Table' property instead of 'Context.Tasks', they are identical.
        //Add some Where conditions...
           if (assignedPersonId.HasValue)
            {
                query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value);
            }
     if (state.HasValue)
            {
                query = query.Where(task => task.State == state);
            }
     return query
                .OrderByDescending(task => task.CreationTime)
                .Include(task => task.AssignedPerson) //Include assigned person in a single query  .ToList();
        }
    }

    上述代码 TaskRepository 派生自 SimpleTaskSystemRepositoryBase 并实现了 ITaskRepository.

    构建应用程序服务层

    应用程序服务层被用于分离表示层和领域层并提供一些界面的样式方法. 在 Application 组件中定义应用程序服务. 首先定义 task 应用程序服务的接口:

    public interface ITaskAppService : IApplicationService
    {
        GetTasksOutput GetTasks(GetTasksInput input);
        void UpdateTask(UpdateTaskInput input);
        void CreateTask(CreateTaskInput input);
    }

    ITaskAppService继承自IApplicationService. 因此ASP.NET Boilerplate自动的提供了一些类的特性(像依赖注入和验证).现在我们来实现ITaskAppService:

    public class TaskAppService : ApplicationService, ITaskAppService
    {
        //These members set in constructor using constructor injection.
        private readonly ITaskRepository _taskRepository;
        private readonly IRepository<Person> _personRepository;
    
        /// <summary> 
        ///In constructor, we can get needed classes/interfaces.
        ///They are sent here by dependency injection system automatically.
        /// </summary> 
        public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
        {
            _taskRepository = taskRepository;
            _personRepository = personRepository;
        }
    
        public GetTasksOutput GetTasks(GetTasksInput input)
        {
        //Called specific GetAllWithPeople method of task repository.
        var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
        //Used AutoMapper to automatically convert List<Task> to List<TaskDto>.
        return new GetTasksOutput
             {
                 Tasks = Mapper.Map<List<TaskDto>>(tasks)
             };
        }
    
        public void UpdateTask(UpdateTaskInput input)
        {
            //We can use Logger, it's defined in ApplicationService base class.
            Logger.Info("Updating a task for input: " + input);
            //Retrieving a task entity with given id using standard Get method of repositories.
            var task = _taskRepository.Get(input.TaskId);
            //Updating changed properties of the retrieved task entity.
            if (input.State.HasValue)
            {
                task.State = input.State.Value;
            } if (input.AssignedPersonId.HasValue)
            {
                task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
            }
        //We even do not call Update method of the repository.
        //Because an application service method is a 'unit of work' scope as default.
        //ABP automatically saves all changes when a 'unit of work' scope ends (without any exception).  }
        public void CreateTask(CreateTaskInput input)
        {
            //We can use Logger, it's defined in ApplicationService class.  
            Logger.Info("Creating a task for input: " + input);
            //Creating a new Task entity with given input's properties
            var task = new Task { Description = input.Description };
            if (input.AssignedPersonId.HasValue)
            {
                task.AssignedPersonId = input.AssignedPersonId.Value;
            }
           //Saving entity with standard Insert method of repositories.
           _taskRepository.Insert(task);
        }
    }

    TaskAppService 使用仓储来操作数据库. 它过在构造函数中注入仓储. ASP.NET Boilerplate实现了依赖注入, 所以我们可以自由的使用构造函数注入和属性注入 (更多的依赖注入章节查看 ASP.NET Boilerplate 文档).

    注意:我们使用PersonRepository来注入IRepository<Person>. ASP.NET Boilerplate会自动给我们的实体创建库. 如果默认的库接口够用的话,我们就不需要在重新定义实体库类型了.

    应用服务层的方法使用了Data Transfer Objects (DTOs)协议. 这是一个非常好的方式,我同样建议大家这么做. 但是你也没必要非这么做不可,如果你能处理你的问题并传输到展示层。.

    GetTasks方法中,我们使用GetAllWithPeople方法,它返回List<Task>类型, 但我可能需要返回一个List<TaskDto>给展现层. 这时候AutoMapper自动的帮助我们把TaskDto对象转换为Task对象.GetTasksInput和GetTasksOutput是特殊的DTOs被定义在GetTasks方法中.

    UpdateTask方法中,我从数据库返回Task(使用IRepository的Get方法)并更新Task属性.注意我并没有调用reponsitory的Update方法. ASP.NET Boilerplate实现工作单元模式. 所以,在应用服务层的所有改变都是以工作单元形式,并最后自动保存到数据库中.

    CreateTask方法中,使用IRepository的Insert方法创建新Task到数据库.

    ASP.NET Boilerplate的ApplicationService类提供一些属性来简化开发应用服务.例如,它定义了Logger给日志. 你也可以自己实现,但要继承IApplicationService接口(注意:ITaskAppService继承自IApplicationService).

    验证

    ASP.NET Boilerplate在服务层方法输入的参数. CreateTask method getsCreateTaskInput as parameter:

    public class CreateTaskInput
    {
       public int? AssignedPersonId { get; set; }
       [Required]
       public string Description { get; set; }
    }

    这里的Description标记是必须输入的意思. 这里你可以使用任何的Data Annotation属性. 如果你需要使用自定义属性, 你可以继承ICustomValidate 接口来实现UpdateTaskInput:

    public class UpdateTaskInput : ICustomValidate
    {
        [Range(1, long.MaxValue)]
        public long TaskId { get; set; }
        public int? AssignedPersonId { get; set; }
        public TaskState? State { get; set; }
        public void AddValidationErrors(List<ValidationResult> results)
        {
           if (AssignedPersonId == null && State == null)
            {
                results.Add(new ValidationResult("Both of AssignedPersonId and State can not be null in order to update a Task!", new[] { "AssignedPersonId", "State" }));
            }
        }
    
        public override string ToString()
        {
          return string.Format("[UpdateTask > TaskId = {0}, AssignedPersonId = {1}, State = {2}]", TaskId, AssignedPersonId, State);
        }
    }

    你可以替换AddValidationErrors方法里面的内容来自定义错误代码.

    处理异常

    注意我们没有做任何的异常处理. ASP.NET Boilerplate 自动的处理了异常, 日志和返回友好的错误信息给客户端. 客户端仅需处理错误信息并显示给用户.  异常处理文档查看.

    构建Web API服务

    我把应用服务层暴露给远程客户端,以便AngularJs能够简单的使用AJAX调用.

    ASP.NET Boilerplate使用自动的方法来提供应该程序服务,即ASP.NET Web API.如DynamicApiControllerBuilder:

    DynamicApiControllerBuilder
        .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem")
        .Build();

    在本段代码中, ASP.NET Boilerplate找所有继承自IApplicationService接口的方法并创建web api controller给每个应用程序服务类. 这里还有几种其他的查找control的方法.我们将在如何使用AJAX调用服务中看到.

    开发SPA

    我要在项目中实现一个单页面的web应用程序. AngularJs(Google开发的)是一个最流行的最火的SPA框架.

    ASP.NET Boilerplate提供了一个模版来简单的使用AngularJs.这个模版有两个页面(Home 和About)能平滑过度. 使用了Twitter的Bootstrap前端框架.ASP.NET Boilerplate它默认定义了English和Turkish两种本地语言(你可以简单的添加删除语言).

    我们先改变路由模版. ASP.NET Boilerplate模版使用AngularUI-Router, 这实际上是标准的AngularJs路由.它提供了路由状态模式. 我们有两个views: task list 和 new task. 所以我们将在app.js中来做如下的定义:

    app.config([ '$stateProvider', '$urlRouterProvider',
        function ($stateProvider, $urlRouterProvider) {
            $urlRouterProvider.otherwise('/');
            $stateProvider
                .state('tasklist', {
                    url: '/',
                    templateUrl: '/App/Main/views/task/list.cshtml',
                    menu: 'TaskList' //Matches to name of 'TaskList' menu in SimpleTaskSystemNavigationProvider  })
                .state('newtask', {
                    url: '/new',
                    templateUrl: '/App/Main/views/task/new.cshtml',
                    menu: 'NewTask' //Matches to name of 'NewTask' menu in SimpleTaskSystemNavigationProvider  });
        }
    ]);

    app.js是javascript的入口文件,用来配置SPA的启动. 注意这里使用的是cshtml示图文件! 一般情况下, AngularJs的显示页面是html. 而ASP.NET Boilerplate使用cshtml文件. 因此强大的razor引擎将会把cshtml生成HTML.

    ASP.NET Boilerplate提供了基础框架来创建和显示菜单.可以用c#或javascript语言来定义菜单. SimpleTaskSystemNavigationProvider类来创建菜单 ,header.js/header.cshtml用来显示菜单.

    首先我们创建一个Angular controllertask列表页面:

    (function() { var app = angular.module('app'); var controllerId = 'sts.views.task.list';
        app.controller(controllerId, [ '$scope', 'abp.services.tasksystem.task',
            function($scope, taskService) { var vm = this;
    
                vm.localize = abp.localization.getSource('SimpleTaskSystem');
    
                vm.tasks = [];
    
                $scope.selectedTaskState = 0;
    
                $scope.$watch('selectedTaskState', function(value) {
                    vm.refreshTasks();
                });
    
                vm.refreshTasks = function() {
                    abp.ui.setBusy( //Set whole page busy until getTasks complete  null,
                        taskService.getTasks({ //Call application service method directly from javascript  state: $scope.selectedTaskState > 0 ? $scope.selectedTaskState : null }).success(function(data) {
                            vm.tasks = data.tasks;
                        })
                    );
                };
    
                vm.changeTaskState = function(task) { var newState; if (task.state == 1) {
                        newState = 2; //Completed  } else {
                        newState = 1; //Active  }
    
                    taskService.updateTask({
                        taskId: task.id,
                        state: newState
                    }).success(function() {
                        task.state = newState;
                        abp.notify.info(vm.localize('TaskUpdatedMessage'));
                    });
                };
    
                vm.getTaskCountText = function() { return abp.utils.formatString(vm.localize('Xtasks'), vm.tasks.length);
                };
            }
        ]);
    })();

    我定义了名为’sts.views.task.list‘的控制器. 这是我的命名习惯(for scalable code-base)但你也可以简单的命名为’ListController’. AngularJs也使用依赖注入. 我们这里注入’$scope‘和’abp.services.tasksystem.task‘.首先是Angular的scope变量再次是ITaskAppService自动创建的的javascript服务代理(我们在’Build Web API services’之前就创建了).

    ASP.NET Boilerplate 提供了基础设施本地语言文件用于服务端和客户端. 

    vm.taks是页面的任务列表.vm.refreshTasks方法内执行了taskService获取task的数据集合.这是在selectedTaskState修改的时候被调用(查看执行使用$scope.$watch).

    正如你所看到的,调用应用服务的方法是如此的简单!这就是ASP.NET Boilerplate的特性.它生成了Web API层和Javascript代理层.因此我们调用应用服务层像调用javascript方法一样. 它完全集成进了AngularJs (使用Angular的$http service).

    我们来看看任务列表的页面:

    <div class="panel panel-default" ng-controller="sts.views.task.list as vm"> 
      <div class="panel-heading" style="position: relative;">
      <div class="row"> 
        <!-- Title --> 
        <h3 class="panel-title col-xs-6"> @L("TaskList") - <span>{{vm.getTaskCountText()}}</span> </h3>
       <!-- Task state combobox -->
        <div class="col-xs-6 text-right">
          <select ng-model="selectedTaskState">
            <option value="0">@L("AllTasks")</option>
            <option value="1">@L("ActiveTasks")</option>
            <option value="2">@L("CompletedTasks")</option>
          </select>
        </div>
      </div>
     </div> 
     <!-- Task list -->
     <ul class="list-group" ng-repeat="task in vm.tasks">
       <div class="list-group-item">
         <span class="task-state-icon glyphicon" ng-click="vm.changeTaskState(task)" ng-class="{'glyphicon-minus': task.state == 1, 'glyphicon-ok': task.state == 2}"></span>
        <span ng-class="{'task-description-active': task.state == 1, 'task-description-completed': task.state == 2 }">{{task.description}}</span>
        <br />
       <span ng-show="task.assignedPersonId > 0"><span class="task-assignedto">{{task.assignedPersonName}}</span>
       </span>
       <span class="task-creationtime">{{task.creationTime}}</span>
      </div>
     </ul>
    </div>

    ng-controller 属性(在第一行) 绑定页面的controller. @L(“TaskList”) 获取”task list”的本地语言文本(服务器端解析html的时候执行). 因为这是个cshtml文件.

    ng-model 绑定combobox和javascript变量. 当变量值改变combobox就会被更新.当改变combobox变量就会被更新. 这是AngularJs的双向绑定.

    ng-repeat 是Angular的另一个指令用于循环集合里面的值. 当集合改变(例如增加值),它会自动更新界面. 这是AngularJs的另一个强大特性.

    注意: 当你应该在页面添加javascript文件 (例如, 添加’task list’控制器)时.可以改成在添加HomeIndex.cshtml模版时添加.

    本地化

    ASP.NET Boilerplate提供了灵活健壮的本地化系统.你可以使用XML文件或者资源文件做为本地化的数据源.你也可以自定义数据源.更多信息可以查看文档. 本示例使用XML文件演示(在web应用项目的Localization文件夹里):

    <?xml version="1.0" encoding="utf-8" ?>
    <localizationDictionary culture="en">
      <texts>
       <text name="TaskSystem" value="Task System" />
       <text name="TaskList" value="Task List" />
       <text name="NewTask" value="New Task" />
       <text name="Xtasks" value="{0} tasks" />
       <text name="AllTasks" value="All tasks" />
       <text name="ActiveTasks" value="Active tasks" />
       <text name="CompletedTasks" value="Completed tasks" />
       <text name="TaskDescription" value="Task description" />
       <text name="EnterDescriptionHere" value="Task description" />
       <text name="AssignTo" value="Assign to" />
       <text name="SelectPerson" value="Select person" />
       <text name="CreateTheTask" value="Create the task" />
       <text name="TaskUpdatedMessage" value="Task has been successfully updated." />
       <text name="TaskCreatedMessage" value="Task {0} has been created successfully." />
     </texts>
    </localizationDictionary>

    使用单元测试

    ASP.NET Boilerplate 是可测试的. 我的另一篇文章展示了如何使用ABP 基本项目集成集成单元测试. 查看文章: Unit testing in C# using xUnit, Entity Framework, Effort and ASP.NET Boilerplate.

    摘要

    在这篇文章, 我阐述了如何在 ASP.NET MVC web 应用中开发N层架构的SPA应用. ASP.NET Boilerplate 使用非常好的方式并且如此简单的创建了应用. 下面的链接可以获得更多信息:

    文章历史

    • 2016-10-26: Upgraded sample project to ABP v1.0.
    • 2016-07-19: Updated article and sample project for ABP v0.10.
    • 2015-06-08: Updated article and sample project for ABP v0.6.3.1.
    • 2015-02-20: Added link to unit test article and updated the sample project
    • 2015-01-05: Updated sample project for ABP v0.5.
    • 2014-11-03: Updated article and sample project for ABP v0.4.1.
    • 2014-09-08: Updated article and sample project for ABP v0.3.2.
    • 2014-08-17: Updated sample project to ABP v0.3.1.2.
    • 2014-07-22: Updated sample project to ABP v0.3.0.1.
    • 2014-07-11: Added screenshot of ‘Enable-Migrations’ command.
    • 2014-07-08: Updated sample project and article.
    • 2014-07-01: First publish of the article.

    引用

    [1] ASP.NET Boilerplate 官网: http://www.aspnetboilerplate.com

  • 相关阅读:
    Ext.MessageBox.show的用法
    DecimalFormat很强大
    java根据模板导出excel和excel的一些知识
    数组分成若干个数组
    SQL SERVER 创建视图
    java 中怎么根据当前时间得到上周一和上周五的日期
    复选框提交后不合格还在选中状态
    sql语句的学习(2)
    sql语句的学习(1)
    利用LinkedList实现洗牌功能
  • 原文地址:https://www.cnblogs.com/chuifeng/p/6037927.html
Copyright © 2011-2022 走看看