吉特仓库管理系统-ORM框架的使用

  最近在园子里面连续看到几篇关于ORM的文章,其中有两个印象比较深刻<<SqliteSugar>>,另外一篇文章是<<我的开发框架之ORM框架>>, 第一个做的ORM是相当的不错的,第二个也是相当的不错, 至少在表面上看起来是这么一回事。至于具体的用法和实践我没有深入的去测试过,所以也不便发表更多的意见,不过这种造*的精神我个人还是比较佩服的, 虽说有时候造*是闲的蛋疼的事情,但是如果你没有早过*你也体会不到造*给你带来的感官感受。目前比较受欢迎的ORM框架肯定是微软系的 EF莫属, 当然之前还出现过类似此种框架的Alinq,这也是相当的不错,不过作者大神貌似不怎么更新了。

  前不久开源了自己开发的吉特仓储管理系统【开源地址:https://github.com/hechenqingyuan/gitwms , 有兴趣可以加入群 88718955,142050808 或者本人QQ 821865130 交流】,其中也用到了ORM框架, 但是这个ORM框架不是市面上所见到的ORM框架,是自己开发的一套ORM框架,因为拉不上台面所以也就没有对外公开过,只是自己做项目的时候一直在用,经过多年的修改维护也算有所小成, 在个人开发的仓储系统中成功应用,而且有着较高的稳定运行记录。最近也在给小范围内给有兴趣开发仓储系统的朋友培训如何快速的开发仓储系统, 今天这里总结一下培训中所讲到的ORM框架。

  一. 为什么会有这个框架

    当年没有Linq to SQL , EF 等ORM框架的时候, 写SQL也还是相当痛苦的一件事,市面上当时有MyBatis框架, 这个ORM是从java移植过来的,好不好用不过多的说, 褒贬不一。但是我个人用下来就是有一个很不爽的就是那个配置SQL,那个SQL的配置文件超级特么的多,多到节操都快碎了一地。后面上班公司又基于微软企业库做了一个类似的ORM框架, 这个也没有改变其配置文件的蛋疼本质,那个谁一怒为红颜,我特么的一怒差点撂挑子不干了。痛定思痛我决定自己来改改这个狗日的配置文件,最终有所成也就成了现在吉特仓储系统中使用的Git.Framework.ORM 框架。

    之前也相关自己要做一款非常不错的ORM组件,要支持MS SQL,MySQL,Oracle,Sqlce,Sqlite 等,不过也是真的尝试过,不过我还是有点异想天开,后面做到了对SQL Server,MySQL,Oracle的支持, 终究觉得做的不是那么好,所以也就坚持不下去了,做出来终究也是就是跟别人装逼,后面放弃了对多数据库的支持,只对SQL Server 进行了重点的支持(可能是我能力有限以及对整体结构规划的缺失,导致对其他的数据库支持性并不是那么好),因为我只是自己用而且我的最终目的不是做一个ORM,而是针对某种业务型的项目来辅助支撑。

  二. ORM中数据库脚本配置问题

  <dataCommand name="Common.GetProceParam" database="GitWMS" commandType="Text">
<commandText>
<![CDATA[
SELECT [SPECIFIC_CATALOG],[SPECIFIC_NAME],[ORDINAL_POSITION],[PARAMETER_MODE],[PARAMETER_NAME],[DATA_TYPE],[CHARACTER_MAXIMUM_LENGTH]
FROM [INFORMATION_SCHEMA].[PARAMETERS]
WHERE [SPECIFIC_NAME]=@SPECIFIC_NAME
]]>
</commandText>
<parameters>
<param name="@SPECIFIC_NAME" dbType="String" direction="Input"/>
</parameters>
</dataCommand>

数据库脚本配置问题

  最初系统中所有的SQL语句都是通过以上方式来配置配置在配置文件中,写了一个比较牛逼的DataCommandManager类用于来解析读取这些SQL, 这些配置都是缓存的。并且直接转化为了ADO.NET中的Command对象,只需要在使用的时候连接数据库并且传入参数执行就可以,这样看起来并不错。说好话这样是挺爽的,SQL 不用硬编码在C#代码中了,可以随时在配置文件中修改, 这是java中一直流传的套路(不懂java,但是看到的现象是这样的),项目越做越庞大问题就来了, 业务越来越复杂SQL越来越多,配置文件多到没法维护了,一个问题调试都不知道从何处还是找起,即使在配置文件命名规范上死下功夫仍然存在大量的问题,比如配置文件name值是一样的重复了,SQL写重复了也没办法知道,除非你知道这个项目里面的所有配置节点。

<dataCommand name="Common.GetProceParam" database="GitWMS" commandType="Text">

  这个配置节点中的name 是配置数据库SQL的唯一键值, SQL的配置文件多达上百个,这个name重复了有时候根本不知道从何处找起,也想过使用工具来查找,但是这样终究是效率不高的。难道我每次写一个SQL都要查找一下name是否存在,当然很多人也会说无所谓嘛,但是我个人觉得这个不应该是工作的重点,更加应该将重心放到业务上去。

  三. ORM 配置重复的SQL

<dataCommand name="Common.GetProceParam" database="GitWMS" commandType="Text">
<commandText>
<![CDATA[
SELECT [SPECIFIC_CATALOG],[SPECIFIC_NAME],[ORDINAL_POSITION],[PARAMETER_MODE],[PARAMETER_NAME],[DATA_TYPE],[CHARACTER_MAXIMUM_LENGTH]
FROM [INFORMATION_SCHEMA].[PARAMETERS]
WHERE [SPECIFIC_NAME]=@SPECIFIC_NAME
]]>
</commandText>
<parameters>
<param name="@SPECIFIC_NAME" dbType="String" direction="Input"/>
</parameters>
</dataCommand> <dataCommand name="Sys.GetProceParam" database="GitWMS" commandType="Text">
<commandText>
<![CDATA[
SELECT [SPECIFIC_CATALOG],[SPECIFIC_NAME],[ORDINAL_POSITION],[PARAMETER_MODE],[PARAMETER_NAME],[DATA_TYPE],[CHARACTER_MAXIMUM_LENGTH]
FROM [INFORMATION_SCHEMA].[PARAMETERS]
WHERE [SPECIFIC_NAME]=@SPECIFIC_NAME
]]>
</commandText>
<parameters>
<param name="@SPECIFIC_NAME" dbType="String" direction="Input"/>
</parameters>
</dataCommand>

配置重复的SQL

  上面这个这两个SQL一模一样,只是name的值不一样,当时多人开发导致你根本无法知道这个SQL是不是重复了,虽然这个对程序运行没有什么影响,但是终究是重复了。而且我几乎不可能判断SQL是不是有重复的。这个就存在一个相当坑爹的事情,要做这个事情还是要花费一点时间,但是没有办法公用,不说每个开发人员都会搞一套,但是这种重复的问题是的的确确存在的问题,我相信做MyBatis开发的肯定深有体会。

public List<ProceMetadata> GetMetadataList(string argProceName)
{
DataCommand command = DataCommandManager.GetDataCommand("Common.GetProceParam");
command.SetParameterValue("@SPECIFIC_NAME", argProceName);
List<ProceMetadata> list = command.ExecuteEntityList<ProceMetadata>(); return list;
}

操作配置文件中的SQL语句

  以上是操作配置文件中C#代码,其实也就比Ado.NET 方便那么一点点,如果加上配置等操作完全不会比Ado.NET简单。上面这种还是个别案例,上面这段SQL用的人估计不会很多,如果涉及到业务问题比如获取某个用户的信息,这个时候技术人员沟通不畅那么就极有可能出现重复的代码。再后来的接盘侠过来了怎么办,我该怎么获取用户的信息,写了这么多获取用户的信息,以前的又不敢动那我再重新写一个吧。

  四. ORM获取用户信息

SELECT * FROM [dbo].[T_USER]

SELECT UserID,[Password] FROM [dbo].[T_USER]

SELECT UserID,UserName FROM [dbo].[T_USER]

SELECT UserID,UserName,Email FROM [dbo].[T_USER]

我要获取用户信息?

  获取用户信息偷懒的办法,我们在查询字段的时候直接SELECT * FROM , * 通配符真是一个好东西,可以少些好多的代码,用起来就是爽。如果从业务和更加严谨的角度来说其实我们不建议使用 * 来查询所有表字段信息。业务环境点要求:我要查询用户编号和用户名,我要查询用户名,用户编号,用户邮箱,我要查询用户所有的属性数据,我要查询用户..... ;  这种需求再正常不过了,假设我们不允许在所有环境下查询所有的数据库属性字段,根据上面的框架来开发的话,那的配置多少个SQL配置节点,我要写多少个DataCommand,这还只是其中一个业务点,一个系统怎么可能只有一个业务点。

  五. 数据字段权限如何处理

<dataCommand name="Common.GetProceParam1" database="GitWMS" commandType="Text">
<commandText>
<![CDATA[
SELECT * FROM [dbo].[T_USER]
]]>
</commandText>
<parameters>
</parameters>
</dataCommand>

获取所有用户数据

<dataCommand name="Common.GetProceParam" database="GitWMS" commandType="Text">
<commandText>
<![CDATA[
SELECT UserID,UserName FROM [dbo].[T_USER]
]]>
</commandText>
<parameters>
</parameters>
</dataCommand>

查询用户编号和用户名

<dataCommand name="Common.GetProceParam" database="GitWMS" commandType="Text">
<commandText>
<![CDATA[
SELECT UserID,UserName,IDCard FROM [dbo].[T_USER]
]]>
</commandText>
<parameters>
</parameters>
</dataCommand>

查询用户身份信息

  在业务系统中有些是特别敏感的信息,比如ERP系统中对价格控制,客户能够看到什么价格,VIP看到什么样的价格,什么级别的领导看到又是另外的价格,人就是这么的坑爹! 我相信这是一个再普通不过的业务场景了,各种价格保持到不同的数据库列中,很多人的做法就是先将数据查询出来,然后在用代码将没有权限的数据隐藏掉,这个也是我个人的一般做法,那么我们能不能在读取数据的时候就控制呢,根据权限查询特定的字段信息。这个肯定不用多说肯定是可以的, 根据上面的这种ORM配置来做 就是上面三种类型的配置,好坑爹啊。

======================ORM就是一个坑爹货啊,根本达不到快速开发效果===========================

 六.彻底一些 ORM

    以上的事情足够蛋疼好久的,每天要花大量的体力劳动时间来配置这些SQL语句,我能不能在不改动原有的结构基础上让这些操作变得更加的简单一些。这是得让我们好好想想的事情,ORM框架的核心点在哪里,那就是解决对象与数据库之间的映射关系以及对象与对象之间的关联关系。先不说DataCommand这个类,我们分析Ado.NET 操作数据库的特点,几个步骤就不说了。两个核心点:(1) SQL语句并且带有占位符(可选) (2) 设置占位符参数 , 仔细想想这两个是有规律的,最起码第二个规律很明显,就是AddParamater() 我们完全可以使用参数的形式循环设置这些占位符参数。

    SQL语句的规则稍微有点复杂:

    (1) 增: insert into table (字段,字段) values(值,值)

    (2) 删: delete from table where 条件

    (3) 改: update table set 字段=值,字段=值 where 条件

    (4) 查: select *(字段) from table where 条件

    解决以上ORM的问题,那么就是如何动态的去替换如上语句的关键字。 这里动态很重要,我们需要动态的去设置动态列以及条件,避免硬编码的去配置SQL语句,动态也就意味着不同的条件生成的SQL是不一样的,不需要配置文件穷举这些SQL语句了。我想ORM框架的开发基本就是这个理吧,只要具体细节如何实现就看对语法的理解以及对框架的设计如何了。

  七. 先说映射的属性

[TableAttribute(DbName = "GitWMS", Name = "TNum", PrimaryKeyName = "ID", IsInternal = false)]
public partial class TNumEntity:BaseEntity
{
public TNumEntity()
{
} [DataMapping(ColumnName = "ID", DbType = DbType.Int32,Length=,CanNull=false,DefaultValue=null,PrimaryKey=true,AutoIncrement=true,IsMap=true)]
public Int32 ID { get; set; } public TNumEntity IncludeID (bool flag)
{
if (flag && !this.ColumnList.Contains("ID"))
{
this.ColumnList.Add("ID");
}
return this;
} [DataMapping(ColumnName = "Num", DbType = DbType.Int32,Length=,CanNull=false,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]
public Int32 Num { get; set; } public TNumEntity IncludeNum (bool flag)
{
if (flag && !this.ColumnList.Contains("Num"))
{
this.ColumnList.Add("Num");
}
return this;
} [DataMapping(ColumnName = "MinNum", DbType = DbType.Int32,Length=,CanNull=false,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]
public Int32 MinNum { get; set; } public TNumEntity IncludeMinNum (bool flag)
{
if (flag && !this.ColumnList.Contains("MinNum"))
{
this.ColumnList.Add("MinNum");
}
return this;
} [DataMapping(ColumnName = "MaxNum", DbType = DbType.Int32,Length=,CanNull=false,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]
public Int32 MaxNum { get; set; } public TNumEntity IncludeMaxNum (bool flag)
{
if (flag && !this.ColumnList.Contains("MaxNum"))
{
this.ColumnList.Add("MaxNum");
}
return this;
} [DataMapping(ColumnName = "Day", DbType = DbType.String,Length=,CanNull=true,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]
public string Day { get; set; } public TNumEntity IncludeDay (bool flag)
{
if (flag && !this.ColumnList.Contains("Day"))
{
this.ColumnList.Add("Day");
}
return this;
} [DataMapping(ColumnName = "TabName", DbType = DbType.String,Length=,CanNull=false,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]
public string TabName { get; set; } public TNumEntity IncludeTabName (bool flag)
{
if (flag && !this.ColumnList.Contains("TabName"))
{
this.ColumnList.Add("TabName");
}
return this;
} [DataMapping(ColumnName = "CompanyID", DbType = DbType.String,Length=,CanNull=false,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]
public string CompanyID { get; set; } public TNumEntity IncludeCompanyID (bool flag)
{
if (flag && !this.ColumnList.Contains("CompanyID"))
{
this.ColumnList.Add("CompanyID");
}
return this;
} }

映射的实体对象

  老生常谈的问题,实在不好意思在拿出来讨论了,不说细节了可以去参考别人的ORM框架或本人的<<.NET ORM>>系列文章,在吉特仓储管理系统中充斥着大量的这样实体对象,应该说贯穿到了整个系统中。

[DataMapping(ColumnName = "CompanyID", DbType = DbType.String,Length=,CanNull=false,DefaultValue=null,PrimaryKey=false,AutoIncrement=false,IsMap=true)]

  ColumnName是映射的列与属性之间的关系,这个是核心也是关键,还有一个就是IsMap 这个属性 这个表示是否映射字段。为了灵活性我当然可以扩展这个实体类,但是我就是不想和数据库字段建立关系,你想怎样。

  一个实体对象映射一张表, 代码中我可以扩展这个实体对象属性,这些属性是和表之间唯一对应的,但是我是可以在查询的时候连接查询字段或者计算出来的字段做映射关系的,我就说了查询是SQL中最难的。增删改在操作的时候都必须和对应的表字段对应,而查询偏偏不是这样的,我查询可以从其他表关联查询出来,我可以取别名,我可以查询自定义新的列, 对象是否处理,那关键就是看IsMap 这个值了。

  实体上有DataMapping 特性标识,说明和数据有映射关系,如果没有则和数据库映射操作没有关系,数据库无论怎么操作都不影响这些数据。 标识IsMap=true的属性 这个是和数据库字段强制关联的(一般用于增删改), 没有这个值默认是false,这个时候查询也会映射到此类属性上.

  八. 定义了一套语法规则

public partial interface ITNum : IDbHelper<TNumEntity>
{
}

数据库表操作接口

public partial class TNumDataAccess : DbHelper<TNumEntity>, ITNum
{
public TNumDataAccess()
{
} }

数据库表操作实现

  传说中的DbHelper, 之前还有人专门写过一篇关于DbHelper的文章,到底要不要叫DbHelper, 我不想谈论此类文章,毕竟我不是搞技术研究的料。此DbHelper 并非传统的DbHelper类,这个是自己改良过的(并非改良,是完全不一样,只是名字相同而已), 深受DDD领域驱动设计的毒害,每个人都这么搞我也就弄了一个似于上面的仓储模式,我水平低你们不要笑话我,我也不知道这个DDD中的仓储是不是正宗的,只是看起来有点像,我此等超低领悟能力的人有点理解不来。因为我看到网上的.NET 相关的文章 DDD仓储模型都是使用EF实现的,千篇一律没有其他之一。

  在系统中我同样会定义很多类似于上面的空的接口和空的实现实现类,基本每个表回对应一个这种模型,如果表很多该怎么办,你懂得, 做程序的虽然不聪明但是也绝对不会傻逼到每个都手写。

public int AddMeasure(MeasureEntity entity)
{
entity.IncludeAll();
int line = this.Measure.Add(entity);
return line;
}

新增一个实体对象

  在实体新增的时候,会将数据保持到实体对应的表中去,其中可能大家比较关注的是哪个IncludeAll() , 这个就是解决上面的指定指定问题的, IncludeAll() 意味着新增的时候会生成所有的字段插入到表中。

entity.Include(alias => new { alias.MeasureName, alias.MeasureNum });

  这段代码表示在表中只新增两个字段,IncludeAll() 是新增所有的字段. 当然如果你在新增的时候数据库设计必填而你没有包含进去,这个时候执行SQL的时候是会报错的。

entity.Include("MeasureName").Include("MeasureNum");

  链式表达式,上面的这段代码和表达式这段代码是等价,在操作新增的时候都是包含了两个字段。只要能够和字段映射的上,你就可以无限次的包含字段关系。

INSERT INTO [dbo].[Product]([SnNum],[BarCode],[ProductName],[Num],[MinNum],[MaxNum],[UnitNum],[UnitName],[CateNum],[CateName],[Size],[Color],[Standard],[Pressure],[GasketFace],[SCH],[Status],[ProcessMode],[Material],[InPrice],[OutPrice],[AvgPrice],[NetWeight],[GrossWeight],[Description],[PicUrl],[IsDelete],[CreateTime],[CreateUser],[StorageNum],[DefaultLocal],[CusNum],[CusName],[SupNum],[SupName],[Display],[Remark],[CompanyID])
VALUES(@SnNum,@BarCode,@ProductName,@Num,@MinNum,@MaxNum,@UnitNum,@UnitName,@CateNum,@CateName,@Size,@Color,@Standard,@Pressure,@GasketFace,@SCH,@Status,@ProcessMode,@Material,@InPrice,@OutPrice,@AvgPrice,@NetWeight,@GrossWeight,@Description,@PicUrl,@IsDelete,@CreateTime,@CreateUser,@StorageNum,@DefaultLocal,@CusNum,@CusName,@SupNum,@SupName,@Display,@Remark,@CompanyID)

  你执行的SQL语句可以是这样,当然你也可以这样

INSERT INTO [dbo].[Product]([SnNum],[BarCode],[ProductName],[Num])
VALUES(@SnNum,@BarCode,@ProductName,@Num)

  该死的穷举配置所有的SQL可能性,可以滚蛋了。

  九. 查询关联的字段

    你的ORM框架支持加载子类集合么,或者自动加载主表的数据么。 NO NO NO, 你特么的不要跟我提为什么不加载字表的集合,别的框架都支持加载子类集合啊?? 好烦 好烦 这种问题。  个人缩写的这个ORM框架的确是不支持自动加载子类集合的,第一 个人能力有限, 第二 做ORM就一定要加载子项么, 我就单表操作不可以么(这里不是说只能操作一张表,是能够连接查询的,但是连接查询出来的结果集也是一张表啊)。 你别傻了, 变通 变通 变通!

public override List<InventoryDetailEntity> GetOrderDetail(InventoryDetailEntity entity)
{
InventoryDetailEntity detail = new InventoryDetailEntity();
detail.IncludeAll();
detail.Where(a => a.OrderSnNum == entity.OrderSnNum)
.And(a => a.CompanyID == this.CompanyID)
; ProductEntity product = new ProductEntity();
product.Include(a => new { Size=a.Size, CateName = a.CateName, Standard = a.Standard, Pressure = a.Pressure, GasketFace = a.GasketFace, SCH = a.SCH, ProductStatus = a.Status, ProcessMode = a.ProcessMode, Material = a.Material, GrossWeight = a.GrossWeight }); detail.Left<ProductEntity>(product, new Params<string, string>() { Item1 = "TargetNum", Item2 = "SnNum" }); List<InventoryDetailEntity> list = this.InventoryDetail.GetList(detail);
return list;
}

左连接查询的相关操作

    在上面的IsMap 属性还记得么,上面说到了他就是为了解决连接查询中字段不能匹配实体的问题(反正就是一个区分到底匹配不匹配). 这是使用这种实体模型的时候,我们要大量去扩展数据库映射模型属性, 有点违法了大家DDD说的数据库模型,业务模型,数据传输模型等分离的原则,什么原则我是不懂了,在有些人眼里肯定是不伦不类了。

product.Include(a => new { Size=a.Size, CateName = a.CateName, Standard = a.Standard, Pressure = a.Pressure, GasketFace = a.GasketFace, SCH = a.SCH, ProductStatus = a.Status, ProcessMode = a.ProcessMode, Material = a.Material, GrossWeight = a.GrossWeight });

    这个左连接查询出来的属性字段,肯定要在实体类InventoryDetailEntity 中有对应的属性字段了。

public partial class InventoryDetailEntity
{
[DataMapping(ColumnName = "ProductName", DbType = DbType.String)]
public string ProductName { get; set; } [DataMapping(ColumnName = "BarCode", DbType = DbType.String)]
public string BarCode { get; set; } [DataMapping(ColumnName = "Size", DbType = DbType.String)]
public string Size { get; set; } [DataMapping(ColumnName = "CateName", DbType = DbType.String)]
public string CateName { get; set; } [DataMapping(ColumnName = "UnitName", DbType = DbType.String)]
public string UnitName { get; set; } //扩展产品属性 [DataMapping(ColumnName = "Standard", DbType = DbType.String)]
public string Standard { get; set; } [DataMapping(ColumnName = "Pressure", DbType = DbType.String)]
public string Pressure { get; set; } [DataMapping(ColumnName = "GasketFace", DbType = DbType.String)]
public string GasketFace { get; set; } [DataMapping(ColumnName = "SCH", DbType = DbType.String)]
public string SCH { get; set; } [DataMapping(ColumnName = "ProductStatus", DbType = DbType.Int32)]
public int ProductStatus { get; set; } [DataMapping(ColumnName = "ProcessMode", DbType = DbType.Int32)]
public int ProcessMode { get; set; } [DataMapping(ColumnName = "Material", DbType = DbType.String)]
public string Material { get; set; } [DataMapping(ColumnName = "GrossWeight", DbType = DbType.Double)]
public double GrossWeight { get; set; }
}

扩展属性字段

    可以和上面的那个实体对象做一下对比,DataMapping 也标识了,没有那么多属性其实都是默认了,IsMap其实就默认了false,说明这些属性和表Inventory 没有直接的对应关系(记住一个宗旨,任何操作都可以看做是一个单表操作)。

  十. 如何解决上面提出的问题

    如果你还不能明白如何解决上述提出的问题,比如多配置,命名重复,权限字段等问题,那我只能说你还没有明白我写的文章,要么就是我写的文章太不入流了,不能让你能够很清楚的明白我的意思。

    我本身不是想做一个ORM框架,我就是想做了一个软件,然后逐步的提高开发效率。其他人我不知道了,自从改进到目前的这个程度,在开发效率上的确提升很多了。自我感觉是良好的。 我深知这个ORM不能跟其他的ORM相比,看别人的ORM简直就是高大上,我做这个ORM完全是为了解决当初的开发遇到的问题以及从自己做吉特仓储系统业务的角度来出发的,所以在很大的程度上是有局限性的。但是我相信在整个框架体系中(包括业务的设计)是可以在很大程度上弥补Git.Framework.ORM 方面的不足。

    技术是为业务服务,是为了解决问题,如果单纯的只是为了做框架而框架,我并不觉得这是一个好的方式,就好比DDD领域驱动设计,这种设计思想是很好的,但是请别千篇一律的的找一个使用了DDD设计思想的技术框架奉为定律, 框架本身就是要灵活多变的,不同的业务体系框架也就会不一样,架构也会不一样。(恕我不懂DDD的精髓在此说了大话)

    以上代码实现在吉特仓储管理系统中都有实现, 总体来说并没有什么技术门槛, 不要迷信这是一个非常好的框架,只是因为这个东西我就是为他而定制的,所以刚好就适应了它。如果有更多的想了解吉特仓储管理系统,可以到github上去下载源码: https://github.com/hechenqingyuan/gitwms    有任何关于仓库系统业务方面的也可以和我交流相互学习,最近也弄了两次吉特仓储管理系统快速开发培训(小范围的),如果有兴趣的朋友,特别是上海的朋友可以近距离的交流吉特仓储系统以及物流方面的知识。

作者:情缘

出处:http://www.cnblogs.com/qingyuan/

关于作者:从事仓库,生产软件方面的开发,在项目管理以及企业经营方面寻求发展之路
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

联系方式: 个人QQ  821865130 ; 仓储技术QQ群 88718955,142050808 ;

吉特仓储管理系统 开源地址: https://github.com/hechenqingyuan/gitwms

上一篇:Qlikview List控件


下一篇:linux下查看端口的连接数