SegmentFault .NET杂谈最新的文章
2019-07-29T15:58:23+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Docker学习笔记--镜像
https://segmentfault.com/a/1190000019906831
2019-07-29T15:58:23+08:00
2019-07-29T15:58:23+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>镜像是Docker三大核心概念中最重要的一个,Dokcer运行容器钱需要本地存在对应的镜像,如果不存在对应的镜像,Doker 会先从默认的镜像仓库下载(如果用户不希望Docker从默认的镜像仓库下载镜像的话,可以使用自定义镜像仓库)。这篇文章将讲解镜像的拉取、查看镜像信息、搜索镜像、删除镜像和镜像的导入导出。</p>
<h4>零、拉去镜像</h4>
<p>要想拉取镜像,就要用到 <strong>pull</strong> 命令,该命令的语法为:</p>
<pre><code class="shell">docker[image]pull NAME[:TAG]</code></pre>
<blockquote>注:pull 命令中参数 <strong>NAME</strong> 表示镜像仓库名称,<strong>TAG</strong> 表示镜像标签(一般表示为版本)</blockquote>
<p>举个例子来看一下pull命令怎么使用。我们从默认的镜像仓库与拉取 Ubuntu 16.04,输入如下命令:</p>
<pre><code class="shell">docker pull ubuntu:16.04</code></pre>
<p>执行上面的命令,将会看到如下输出:</p>
<p><img src="/img/remote/1460000019906834?w=774&h=206" alt="Z7ug0I.png" title="Z7ug0I.png"></p>
<blockquote>注1:如果不显示指定TAG,Docker默认使用latest标签,下载镜像仓库中最新的镜像<p>注2:latest标签会随着最新版本的变化而变化,例如昨天某镜像的版本是 1.0 ,今天上传了2.0版本的镜像,那么latest标签指的就是2.0这个版本</p>
</blockquote>
<p>我们从前面的图中可以看到,在镜像下载过程中出现了4行,这四行代表着镜像文件有4个层,每层的唯一id就是每行开头的那串字符串(例如:35b42117c431)。当不同的镜像存在相同的层时,本地仅存出一份内容,这样就叫少了存储空间。</p>
<p>一般情况下,如果是从官方仓库注册服务器下载镜像文件时是不需要加上仓库注册服务器地址的,但是如果是从非官方仓库注册服务器下载的话,就需要加上注册服务器地址,例如我们要从阿里云下载ubuntu16.04的镜像,那么我们应该这样写命令:</p>
<pre><code class="shell">docker pull registry.cn-shanghai.aliyuncs.com/ubuntu:16.04</code></pre>
<p>常用的pull命令参数有如下两个:</p>
<table>
<thead><tr>
<th align="left">参数</th>
<th align="left">描述</th>
<th align="left">默认值</th>
</tr></thead>
<tbody>
<tr>
<td align="left">-a,-all-tags=true\</td>
<td align="left">false</td>
<td align="left">是否获取仓库中所有镜像</td>
<td>false</td>
</tr>
<tr>
<td align="left">-disable-content-trust</td>
<td align="left">取消镜像内荣校验</td>
<td align="left">true</td>
</tr>
</tbody>
</table>
<h4>一、查看镜像信息</h4>
<p>查看镜像信息所用到的命令是 <strong>images</strong> 、 <strong>ls</strong> 和 <strong>inspect</strong> 命令。比如要查看当前电脑上存在的镜像,可以这么做:</p>
<pre><code class="shell">docker images</code></pre>
<p>或者</p>
<pre><code class="shell">docker image ls</code></pre>
<p>执行上面的命令,将会列出当前系统中存在的镜像,如下图</p>
<p><img src="/img/remote/1460000019906835" alt="Z7uWAP.png" title="Z7uWAP.png"></p>
<p>从上图中可以看出如下信息:</p>
<ul>
<li>REPOSITORY:镜像来源</li>
<li>TAG:镜像标签,用于标记来自同一个仓库的不同镜像</li>
<li>IMAGE ID:镜像ID,镜像的唯一标识</li>
<li>CREATED:创建时间</li>
<li>SIZE:镜像大小</li>
</ul>
<blockquote>注1:镜像ID是很重要的信息,因为它是镜像的唯一标识,我们在操作镜像时需要用到它。一般情况下我们只会输入镜像ID的前N位就可以区分一个镜像了。<p>注2:镜像的大小只是代表了镜像的逻辑体积大小。由于相同镜像层在本地只会存储一份,因测镜像在物理上占用的空间小于各个镜像的逻辑体积之和。</p>
</blockquote>
<p><strong>images</strong> 和 <strong>ls</strong> 命令常用的参数选项如下:</p>
<table>
<thead><tr>
<th align="left">参数</th>
<th align="left">描述</th>
<th align="left">默认值</th>
</tr></thead>
<tbody>
<tr>
<td align="left">-a,--all=true\</td>
<td align="left">false</td>
<td align="left">列出所有镜像文件</td>
<td>false</td>
</tr>
<tr>
<td align="left">--digests=true\</td>
<td align="left">false</td>
<td align="left">列出镜像数字摘要值</td>
<td>false</td>
</tr>
<tr>
<td align="left">-f,--filter=[]</td>
<td align="left">根据过滤而条件列出镜像</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">--format=""</td>
<td align="left">控制输出格式</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">--no-trunc=true\</td>
<td align="left">false</td>
<td align="left">阶段输出结果中过长的字符</td>
<td>true</td>
</tr>
<tr>
<td align="left">-q,--quiet=true\</td>
<td align="left">false</td>
<td align="left">只输出ID</td>
<td>false</td>
</tr>
</tbody>
</table>
<p>我们有时候需要获取镜像详细的信息,这时我们可以使用 <strong>inspect</strong> 命令,语法格式为:</p>
<pre><code class="shell">docker inspect [OPTIONS] NAME|ID [NAME|ID...]</code></pre>
<p>例如我们要查看刚才我们拉取的Ubuntu16.04镜像的详细信息,可以这么操作:</p>
<pre><code class="shell">docker inspect 13c9</code></pre>
<p>运行上面的命令后,我们会看到镜像的详细信息以json的形式打印出来,如图:<br><img src="/img/remote/1460000019906836" alt="ZqpAR1.png" title="ZqpAR1.png"></p>
<p>我们不仅可以查看镜像的详细信息,同样我们也可以查看层的详细信息,这时就需要用到 <strong>history</strong> 命令,语法如下:</p>
<pre><code class="shell">docker history [OPTIONS] NAME|ID</code></pre>
<p>例如我们查看Ubuntu16.04镜像层的详细信息:</p>
<pre><code class="shell">docker history 13c9</code></pre>
<p>运行命令,将打印出层的详细信息。<br><img src="/img/remote/1460000019906837?w=1100&h=195" alt="Zqprzq.png" title="Zqprzq.png"></p>
<h4>二、搜索镜像</h4>
<p>我们可以使用 <strong>search</strong> 命令搜索仓库中的镜像,语法为:</p>
<pre><code class="shell">docker search [option] keyword</code></pre>
<p>option 常用的参数如下:</p>
<table>
<thead><tr>
<th align="left">参数</th>
<th align="left">描述</th>
<th align="left">默认值</th>
</tr></thead>
<tbody>
<tr>
<td align="left">-f, --filter</td>
<td align="left">过滤输出内容</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">--format</td>
<td align="left">格式化输出内容</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">--limit</td>
<td align="left">限制输出结果个数</td>
<td align="left">25</td>
</tr>
<tr>
<td align="left">--no-trunc</td>
<td align="left">不揭短输出结果</td>
<td align="left">fasle</td>
</tr>
</tbody>
</table>
<p>我们举个例子来看一下,我们要搜索仓库中包含mysql的镜像,代码如下:</p>
<pre><code class="shell">docker search mysql</code></pre>
<p>运行上面的命令,将会列出包含mysql关键字的镜像:<br><img src="/img/remote/1460000019906838" alt="ZqALv9.png" title="ZqALv9.png"></p>
<h4>三、删除/清除镜像</h4>
<ol><li>删除镜像</li></ol>
<p>我们删除镜像的时候可以利用 <strong>镜像标签</strong> 、 <strong>镜像ID</strong> 来删除镜像。删除镜像的语法为:</p>
<pre><code class="shell">docker rmi IMAGE_NAME|IMAGE_ID</code></pre>
<p>例如我们利用镜像标签删除u16.04这个镜像:</p>
<pre><code class="shell">docker rmi u16.04</code></pre>
<p>运行上面的命令,u16.04这个镜像将会被删除。</p>
<blockquote>注1:u16.04 这个镜像是我利用ubuntu:16.04 这个镜像通过添加镜像标签创建的<p>注2:当使用镜像ID删除镜像时,会先删除所有指向该镜像的标签,然后再删除该镜像文件本身</p>
<p>注3:如果存在通过该镜像创建的容器时,镜像文件无法删除。如果要强制删除镜像时,可以使用 -f 参数:<code>docker rmi -f u16.04</code> ,但是不建议强制删除镜像。</p>
</blockquote>
<ol><li>清楚镜像</li></ol>
<p>经过一段时间,系统中会存在临时/不再使用的镜像文件,那么我们可以通过 <strong>prune</strong> 命令清理镜像,语法如下:</p>
<pre><code class="shell">docker image [options] prune</code></pre>
<p>option 常用的参数如下:</p>
<table>
<thead><tr>
<th align="left">参数</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">-a,-all</td>
<td align="left">删除所有无用镜像</td>
</tr>
<tr>
<td align="left">-filter</td>
<td align="left">只删除符合过滤条件的镜像</td>
</tr>
<tr>
<td align="left">-f,-force</td>
<td align="left">强制删除镜像</td>
</tr>
</tbody>
</table>
<h4>四、导入导出镜像</h4>
<ol><li>导出</li></ol>
<p>如果要将镜像导出可使用 <strong>save</strong> 命令,语法如下:</p>
<pre><code class="shell">docker save image -o file</code></pre>
<p>-o 表示将镜像导出到tar文件,例如我们将 ubuntu:16.04导出到 u1604.tar 文件中:</p>
<pre><code class="shell">docker save 13c9 -o u1604.tar</code></pre>
<p>运行上面命令后,ubuntu:16.04就导入到了ub1604.tar文件中:<br><a href="https://link.segmentfault.com/?enc=4l1l4FpEyRSD1%2FrFfQshMQ%3D%3D.nOtXVSXC0D8IJEK%2FoOCh9saHCuu5OZ5rqwEYGxlBenI%3D" rel="nofollow"><img src="/img/remote/1460000019906839?w=571&h=102" alt="ZqZp1e.png" title="ZqZp1e.png"></a></p>
<ol><li>导入</li></ol>
<p>如果要将镜像导出可使用 <strong>load</strong> 命令,语法如下:</p>
<pre><code class="shell">docker load -i file</code></pre>
<p>-i 表示将要导入到docker的镜像tar文件,例如我们将 ub1604.tar导入到docker中:</p>
<pre><code class="shell">docker load -i u1604.tar</code></pre>
<p>运行上面命令后,ub1604.tar将被导入到docker中<br><img src="/img/remote/1460000019906840" alt="ZqZKXj.png" title="ZqZKXj.png"></p>
<h4>五、上传镜像</h4>
<p>如果是上传到官方仓库,那么需要先注册,然后才可以利用 <strong>push</strong> 命令上传镜像,语法如下:</p>
<pre><code class="shell">docker push NAME[:TAG]|[REGISTRY_HOST[:REGISTRY_PORT]/]</code></pre>
<p>例如我们将ubuntu:16.04上传到官方服务器上:</p>
<pre><code class="shell">docker push 13c9</code></pre>
docker学习笔记-小知识
https://segmentfault.com/a/1190000019906761
2019-07-29T15:54:20+08:00
2019-07-29T15:54:20+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、加入docker用户组</h4>
<p>在使用非管理员账户登录操作系统,操作docker的时候需要切换到管理员的身份,每次都这样操作会比较麻烦,所以可以将当前用户加入到 <strong><em>docker 用户组</em></strong>,代码如下:</p>
<pre><code class="shell">sudo usermod -aG docker USER_NAME</code></pre>
<h4>一、配置启动项</h4>
<p>Doker 服务启动调用的是 <strong><em>dockerd 命令</em></strong>,dockerd 命名支持多种启动参数。例如启动Docker 的 debug模式并监听8011端口,代码如下:</p>
<pre><code class="shell">docker -D -H tcp://127.0.0.1:8011</code></pre>
<p>上面的参数可以写入docker文件 <strong><em>daemon.json</em></strong> 中,文件位于 <strong><em>/etc/docker</em></strong>,配置如下:</p>
<pre><code class="json">{
"debug":true,
"hosts":["tcp://127.0.0.1:8011"]
}</code></pre>
<p>同样可以将上面的参数配置写入到docker配置文件中,以ubuntu为例,docker默认的配置文件位于 <strong><em>/etc/default/docker</em></strong> ,打开配置文件修改 <strong><em>DOCKER_OPTS</em></strong>,修改如下:</p>
<pre><code>DOCKER_OPTS="$DOCKER_OPTS -H tcp://0.0.0.0:8011 -H unix://var/run/docker.sock"</code></pre>
重写、重载和隐藏
https://segmentfault.com/a/1190000019906531
2019-07-29T15:43:06+08:00
2019-07-29T15:43:06+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>重写、重载和隐藏是经常在开发中用到的知识点,今天我们在这里来讲一下他们三个的区别。</p>
<h4>零、重写</h4>
<p>所谓重写,就是在继承中,子类重新定义父类的方法,这里需要注意的是:</p>
<ol>
<li>必须在有继承关系的类中重写;</li>
<li>子类重写的方法名和参数列表必须与父类的方法名和参数列表一致;</li>
<li>父类方法用 <strong><em>virtual</em></strong> 修饰;</li>
<li>子类方法用 <strong><em>override</em></strong> 修饰;</li>
<li>重写一般用于接口实现和继承类的方法改写;</li>
<li>不管访问父类还是子类的方法,都是调用的子类的方法。</li>
</ol>
<p>我们通过代码来看一下重写:</p>
<ol><li>创建父类 <strong>Cat</strong>
</li></ol>
<pre><code class="csharp">public class Cat
{
public virtual void Call()
{
Console.WriteLine("小猫喵喵叫");
}
}</code></pre>
<ol><li>创建子类 <strong>BlackCat</strong>
</li></ol>
<pre><code class="csharp">public class BlackCat:Cat
{
public override void Call()
{
Console.WriteLine("黑猫喵喵叫!");
}
}</code></pre>
<ol><li>调用</li></ol>
<pre><code class="csharp">class Program
{
static void Main(string[] args)
{
Cat cat = new BlackCat();
//输出 “黑猫喵喵叫!”
cat.Call();
BlackCat blackCat = new BlackCat();
//输出 “黑猫喵喵叫!”
blackCat.Call();
Console.Read();
}
}</code></pre>
<h4>一、重载</h4>
<p>所谓重载就是在同一个作用域中,存在多个名称相同但参数列表不同的方法,通过穿点不同的实参来决定具体调用哪个方法。这里有一点需要注意:返回值不同不能称为重载。同样我们来通过代码看一下重载:</p>
<ol><li>定义三个名称相同但参数不同的方法</li></ol>
<pre><code class="csharp">public class Person
{
public void Info()
{
Console.WriteLine("我是小明");
}
public void Info(string sex)
{
Console.WriteLine("我是小明,性别" + sex);
}
public void Info(string name, int age, string sex)
{
Console.WriteLine("我是" + name + ",今年" + age + "岁,性别" + sex);
}
}</code></pre>
<ol><li>调用</li></ol>
<pre><code class="csharp">class Program
{
static void Main(string[] args)
{
Person person = new Person();
//输出“我是小明”
person.Info();
//输出 “我是小明,性别男”
person.Info("男");
//输出 “我是小红,今年10岁,性别女”
person.Info("小红", 10, "女");
Console.Read();
}
}</code></pre>
<h4>二、隐藏</h4>
<p>隐藏就比较有意思了,隐藏又称覆盖,父类方法中不做声明,子类方法中通过 <strong>new</strong> 关键字讲方法隐藏,但是他不会改变父类方法,也就是说:访问父类,调用父类方法,访问子类,调用子类方法。这个跟重写不同。我们在使用隐藏的时候需要注意以下几点:</p>
<ol>
<li>隐藏的方法的标志必须要和被隐藏的方法的标志完全匹配;</li>
<li>隐藏的方法的返回值必须和被隐藏的方法的返回一致;</li>
<li>隐藏的方法所抛出的异常必须和被隐藏方法的所抛出的异常一致,或者是其子类;</li>
<li>被隐藏的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行隐藏。</li>
</ol>
<p>同样我们通过代码来看一下:</p>
<ol><li>定义父类</li></ol>
<pre><code class="csharp">public class Cat
{
public void Call()
{
Console.WriteLine("小猫喵喵叫!");
}
}</code></pre>
<ol><li>定义子类</li></ol>
<pre><code class="csharp">public class BlackCat : Cat
{
new public void Call()
{
Console.WriteLine("黑猫喵喵叫!");
}
}</code></pre>
<ol><li>调用</li></ol>
<pre><code class="csharp">class Program
{
static void Main(string[] args)
{
Cat cat = new BlackCat();
//输出 “小猫喵喵叫!”
cat.Call();
BlackCat blackCat = new BlackCat();
//输出 “黑猫喵喵叫!”
blackCat.Call();
Console.Read();
}
}</code></pre>
<blockquote>注意:隐藏主要用在无法改变父类方法的情况下</blockquote>
<h4>三、总结</h4>
<p>根据上述讲解总结出如下区别:</p>
<ol>
<li>重载是方法名相同,参数(个数/类型)不同;</li>
<li>重写是重新定义父类的方法,需要用到 <strong>virtual</strong> 和 <strong>override</strong>;</li>
<li>隐藏不改变父类的方法。</li>
</ol>
通俗易懂的ref和out区别
https://segmentfault.com/a/1190000019906493
2019-07-29T15:41:47+08:00
2019-07-29T15:41:47+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p><strong>ref</strong> 和 <strong>out</strong> 是C#开发中经常用到的两个关键字,但是很多人没有搞清楚这两个关键字的具体区别,下面我们来说一下这两个关键的区别。</p>
<h4>零、 ref 与 out 的异同</h4>
<ol>
<li>
<p>相同:</p>
<ul>
<li>都是按地址传递;</li>
<li>使用后都将改变原来参数的值;</li>
<li>编译时处理方式相同;</li>
<li>属性不能作为参数传递。</li>
</ul>
</li>
<li>
<p>不同:</p>
<ul>
<li>
<strong>ref</strong> 将参数值传进方法, <strong>out</strong> 无法将参数值传入方法;</li>
<li>
<strong>ref</strong> 传入参数前必须先初始化, <strong>out</strong> 不必在参数传输方法前初始化,但必须在方法中初始化;</li>
<li>
<strong>ref</strong> 用在需要被调用的方法修改调用者的引用的时候, <strong>out</strong> 用在需要返回多个结果的地方。</li>
</ul>
</li>
</ol>
<h4>一、代码演示</h4>
<p><strong>ref</strong> 将参数值传进方法</p>
<pre><code class="csharp">static void Main(string[] args)
{
//初始化
int number = 50;
Console.WriteLine("调用方法前 number 值:" + number);
RefFunction(ref number);
Console.WriteLine("调用方法后 number 值:" + number);
Console.Read();
}
// 传入的参数值是 50 ,方法中使用的num值也是50
static void RefFunction(ref int num)
{
num = num / 2;
}</code></pre>
<p>输出结果如下图所示:<br><img src="/img/remote/1460000019906496?w=830&h=486" alt="Zh3gpQ.png" title="Zh3gpQ.png"></p>
<p><strong>out</strong> 无法将参数值传入方法</p>
<pre><code class="csharp">static void Main(string[] args)
{
int number = 50;
Console.WriteLine("调用方法前 number 值:" + number);
OutFunction(out number);
Console.WriteLine("调用方法后 number 值:" + number);
Console.Read();
}
// 无法将的参数值50传入 ,但是必须在方法中初始化
static void OutFunction(out int num)
{
//初始化
num = 120;
num = num / 2;
}</code></pre>
<p>输出结果如下图所示:<br><a href="https://link.segmentfault.com/?enc=G6PzqjKGPn2%2B3VTYfOUSnw%3D%3D.r0OGFdyjjc0b6ZySigLPWfUKdULsySb3mlPgUAhULpI%3D" rel="nofollow"><img src="/img/remote/1460000019906497?w=677&h=419" alt="Zh8EnI.png" title="Zh8EnI.png"></a></p>
<blockquote>小拓展:如果一个方法采用ref或out参数,而另一个方法不采用这两类参数,则可以进行重载。如下代码示例就是重载,可以通过编译:</blockquote>
<pre><code class="csharp">static void Function(out int num)
{
num = 120;
num = num / 2;
}
static void Function(int num)
{
num = num / 2;
}</code></pre>
Entity Framework 迁移
https://segmentfault.com/a/1190000019906441
2019-07-29T15:39:06+08:00
2019-07-29T15:39:06+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>这一篇文章主要讲解EF的迁移,我们前面的文章一直是使用新增数据的方式生成数据库,但是在实际开发过程中,我们会使用代码迁移的方式生成数据库,下面我们来讲解一下代码迁移。</p>
<h4>零、代码迁移命令</h4>
<p>我们在进行代码迁移的时候经常会用到如下命令:</p>
<ol><li>Enable-Migrations:在项目中启动代码迁移;</li></ol>
<ul>
<li>-ContextTypeName :指定要使用的上下文,默认情况下该参数可以省略,这时EF将查找项目中单个的上下文,这里不建议在有多个上下文的项目中省略该参数;</li>
<li>-EnableAutomaticMigrations :是否禁用自动迁移,此参数可以省略,默认值是禁止自动迁移,建议在开发中将此参数省略;</li>
</ul>
<ol>
<li>Add-Migration:对已经挂起的模型改变搭建基础架构;</li>
<li>Update-Database:将挂起的模型应用到数据库中,并保持模型同步。</li>
</ol>
<p>上面的命令顺序就是我们开发时代码迁移命令执行的顺序。</p>
<blockquote>注1:当执行 Add-Migration 命令后生成的模型状态为挂起状态<p>注2:如果要查看模型是否已经迁移到数据库,可使用 <strong>Get-Migrations</strong> 命令查看。</p>
</blockquote>
Entity Framework 继承映射
https://segmentfault.com/a/1190000019906386
2019-07-29T15:36:23+08:00
2019-07-29T15:36:23+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>继承是面向对象开发时经常用到的,但是SQL Server 数据库不具备继承,那么怎么办能?我们可以利用如下三种方法:</p>
<ol>
<li>TPH(Table per Hierachy):对SQL架构进行非规范化来表示多态,使用鉴别列对类型区分;</li>
<li>TPT(Table per Type):用外键表示继承关系</li>
<li>TPC(Table per Concete class):完全丢弃多态和继承</li>
</ol>
<h4>零、TPH</h4>
<p>TPH是将整个类层次映射到但表中,该表包含所有类中的所有属性,特定行表示的具体子类通过 <strong>discriminator</strong> 来标识区分。TPH是Code First 默认人的继承策略,没有表示C#的多态特性,优点是不需要联合查询,是最简单的策略。缺点是除主键和标识列 <strong>discriminator</strong> 外,其他的列都是可为空。父类中的某些属性对于子类来说并不是必需的,因此Code First 会将改属性创建为可空列。TPH策略因为有 <strong>discriminator</strong> 列的存在,维护性不强,因此违反了第三范式。</p>
<h4>一、TPT</h4>
<p>TPT是常用的策略,通过外键来表示继承,父类和子类分别位于不同的表中,子类表包含自身属性列和父类表的外键,并将父表的外键作为子类表的主键。定义TPT继承策略需要在上下文中进行如下配置:</p>
<pre><code class="csharp">modelBuilder.Entity<Person>().ToTable("Person");
modelBuilder.Entity<Woman>().ToTable("Woman");</code></pre>
<p>TPT的有点是将模型进行了扁平化,对父类的修改或对子类的添加都只是操作各自的表。与父类的多态关联将被表示为引用父类表的外键。缺点在于性能极差,如果要进行查询就要多表连接查询。</p>
<h4>二、 TPC</h4>
<p>TPC是不被推荐的策略,因为他会为每个子类创建一个表,并且将父类中的所有属性映射进每个子类表中。创建TPC映射,需要在上下文中进行如下定义:</p>
<pre><code class="csharp">modelBuilder.Entity<Person>().Map(p=>{
p.MapInheritedProperties();
p.ToTable("Person");
});
modelBuilder.Entity<Woman>().Map(p=>{
p.MapInheritedProperties();
p.ToTable("Woman");
});</code></pre>
<blockquote>注:参与TPC继承层次结构的表不共享主键,因此插入到子类表中的数据会存在重复的主键。要解决这个问题需要为每个表指定不同的标识,也可以关闭主键属性的标识。</blockquote>
<p>下面总结一下以上三种策略的使用场景</p>
<table>
<thead><tr>
<th align="left">策略</th>
<th align="left">场景</th>
</tr></thead>
<tbody>
<tr>
<td align="left">TPC</td>
<td align="left">不需要多表关联查询或者很少查询父类数据,并且没有与父类关联的类</td>
</tr>
<tr>
<td align="left">TPH</td>
<td align="left">需要多表关联查询,且子类的属性较少</td>
</tr>
<tr>
<td align="left">TPT</td>
<td align="left">需要多表关联查询,且子类的属性很多</td>
</tr>
</tbody>
</table>
Entity Framework 私有属性映射
https://segmentfault.com/a/1190000019906351
2019-07-29T15:33:47+08:00
2019-07-29T15:33:47+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>这篇文章纯属自娱自乐,因为在实际开发中用到私有化属性映射的可能性几乎为0。在EF中默认映射的是具有 <strong>public</strong> 修饰符的属性,但是如果是 <strong>internal</strong> 、 <strong>private</strong> 和 <strong>protected</strong> 修饰的属性要映射进数据库怎么做呢?下面我们分别来讲解一下。</p>
<h4>零、 internal 属性映射</h4>
<p>我们先创建代码实体类代码</p>
<pre><code class="csharp">{
public int Id { get; set; }
public string Name { get; set; }
internal int Age { get; set; }
}</code></pre>
<p>在上面的实体类代码中,我们看到 <strong>Age</strong> 属性的修饰符是 <strong>internal</strong> , EF是不会映射 <strong>Age</strong> 属性的,如果需要EF映射该属性就必须显示指定映射该属性。代码如下:</p>
<pre><code class="csharp">public class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
ToTable("User");
HasKey(p => p.Id);
Property(p => p.Age);
}
}</code></pre>
<p>上面的代码中 <code>Property(p => p.Age);</code> 就是显示指定Age属性映射,如果去掉该行代码,EF将不会将 Age 属性映射进数据库。</p>
<h4>一、 private 和 protected 属性映射</h4>
<p>要让 private 和 protected 属性映射,需要用到部分类。我们将前面的实体类修改一下:</p>
<pre><code class="csharp">public partial class User
{
public int Id { get; set; }
public string Name { get; set; }
private int Age { get; set; }
}</code></pre>
<p>上面的代码在类上面添加了 <strong>partial</strong> 关键字(这个关键字就是部分类关键字),并且我们将 <strong>Age</strong> 属性的修饰符修改为 <strong>private</strong>,下面我们就来看看如何映射 <strong>Age</strong> 属性。</p>
<ol><li>首先我们定义一个User的部分类:</li></ol>
<pre><code class="csharp">public partial class User
{
public class PrivatePropertyExtension
{
public static readonly Expression<Func<User, int>> expression =
p => p.Age;
}
}</code></pre>
<ol><li>然后再UserMap 类中添加如下代码:</li></ol>
<pre><code class="csharp">Property(User.PrivatePropertyExtension.expression);</code></pre>
<p>代码完成后,Age 属性将会被映射。protected属性同理。当然,上面第二部的方法我们也可以使用 EF的API 去实现,我们需要在上下文派生类中的 <strong>OnModelCreating</strong> 中设置将所有非公有属性映射到数据库:</p>
<pre><code class="csharp">modelBuilder.Types().Configure(p =>
{
var noPublic = p.ClrType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
foreach(var d in noPublic)
{
p.Property(d).HasColumnName(d.Name);
}
});</code></pre>
Entity Framework 小知识(五)
https://segmentfault.com/a/1190000019906282
2019-07-29T15:31:21+08:00
2019-07-29T15:31:21+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>在 <strong>多对多关系映射</strong> 中关联表是EF自动生成的。但有时候我们需要显示定义关联表。我们可以按照如下步骤定义(继续使用多对多关系映射这篇文章饿代码):</p>
<ol><li>定义关联表类:</li></ol>
<pre><code class="csharp">public class StudentCourses : BaseEntity
{
public int StudentId { get; set; }
public virtual Student Student { get; set; }
public int CourseId { get; set; }
public virtual Course Course { get; set; }
}</code></pre>
<ol><li>修改 <strong>Student</strong> 和 <strong>Courses</strong> 类中的导航属性:</li></ol>
<pre><code class="csharp">public class Student:BaseEntity
{
public string Name { get; set; }
public int Age { get; set; }
public virtual ICollection<StudentCourses> StudentCourses { get; set; }
}
public class Course : BaseEntity
{
public string Name { get; set; }
public string TeacherName { get; set; }
public virtual ICollection<StudentCourses> StudentCourses { get; set; }
}</code></pre>
<ol><li>修改 <strong>StudentMap</strong> 和 <strong>CoursesMap</strong> 映射:</li></ol>
<pre><code class="csharp">public class StudentsMap : EntityTypeConfiguration<Student>
{
public StudentsMap()
{
//表名称
ToTable("Students");
//主键
HasKey(p => p.Id);
//设置主键自增长
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
//设置要映射的数据
Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(p => p.Age);
Property(p => p.CreateDateTime);
//设置关系
HasMany(p => p.StudentCourses)
.WithRequired(p => p.Student)
.HasForeignKey(p => p.StudentId);
}
}
public class CourseMap : EntityTypeConfiguration<Course>
{
public CourseMap()
{
ToTable("Coureses");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(p => p.TeacherName);
Property(p => p.CreateDateTime);
HasMany(p => p.StudentCourses)
.WithRequired(p => p.Course)
.HasForeignKey(p => p.CourseId);
}
}</code></pre>
<p>运行代码,查看数据库发现生成了同样的数据库,多对多关系也正确的反映出来。</p>
Entity Framework 实体状态
https://segmentfault.com/a/1190000019906245
2019-07-29T15:28:56+08:00
2019-07-29T15:28:56+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>从今天开始我们开始讲解EF中的实体状态和数据操作,这篇文章先讲解实体状态。<br>我们通过前面的学习,知道EF通过上下位负责跟踪实体的状态,实体状态的位置是在命名空间 <strong>System.Dat.Entity</strong> 里的 <strong>EntityState</strong>,具体状态有如下5种:</p>
<ol>
<li>Detached</li>
<li>Unchanged</li>
<li>Added</li>
<li>Deleted</li>
<li>Modified</li>
</ol>
<p>下面我们分辨来讲解一下</p>
<h4>零、Detached</h4>
<p>有时候我们只需要实体显示,而不需要实体更新,为了提高性能,我们就就不需要EF上下文对实体进行跟踪,这个时候我们就用到了 <strong>Detached</strong> 状态。我们只需要在查询的时候使用 <strong>AsNoTracking()</strong> 来世的查询出来的对象是 <strong>Detached</strong> 状态。实例代码如下:</p>
<pre><code class="csharp">var person = db.Person.AsNoTracking().Where(p=>p.Id==1).FirstOrDefault();</code></pre>
<blockquote>注:因为 <strong>AsNoTracking</strong> 是DbQuery类的方法,因此要首先调用 <strong>AsNoTracking</strong> 方法。</blockquote>
<h4>一、Unchanged</h4>
<p>在这个状态下实体被上下文追踪,但是数据库中的值没有发生任何改变。如果实体不存在于数据库,但是该实体要被上下文追踪,同时实体值未发生改变,这个时候就可以通过 <strong>Attach</strong> 进行附加追踪,然后将实体状态标记为 <strong>Unchanged</strong> 。 实例代码如下:</p>
<pre><code class="csharp">using (var db = new EFDbContext())
{
var user =new User()
{
Name = "张三",
Age = 12
}
db.User.Attach(user);
db.Entry(user).State = EntityState.Unchanged;
db.SaveChanges();
}</code></pre>
<h4>二、Added</h4>
<p>当进行新增操作时就会用到 <strong>Added</strong> 状态。标记为 <strong>Added</strong> 状态时,表明尸体上下文被追踪但是不存在于数据库中,当我们调用 <strong>SaveChanges</strong> 方法时数据将保存进数据库。如果要将实体状态标记为该状态,可以使用两种方法:</p>
<ol><li>间接标记,通过 <strong>Add</strong> 方法调用,示例代码如下:</li></ol>
<pre><code class="csharp">using (var db = new EFDbContext())
{
var user = new User()
{
Name = "张三"
Age = 12
}
db.User.Add(user);
db.SaveChanges();
}</code></pre>
<ol><li>显式标记,通过调用 <strong>Entry</strong> 方法,示例代码如下:</li></ol>
<pre><code class="csharp">using (var db = new EFDbContext())
{
var user = new User()
{
Name = "张三"
Age = 12
}
db.Entry(user).State = EntityState.Added;
db.SaveChanges();
}</code></pre>
<h4>三、Deleted</h4>
<p>如果需要将实体从数据库中删除,可以使用 <strong>Deleted</strong> 状态,当调用 <strong>SaveChanges</strong> 方法时数据将会从数据库中删除。和 <strong>Added</strong> 状态一样,删除实体可以使用两种方法:</p>
<ol><li>通过调用 <strong>Remove</strong> 或者是 <strong>RemoveRange</strong> 方法,示例代码如下:</li></ol>
<pre><code class="csharp">using (var db = new EFDbContext())
{
var user = db.User..Where(p => p.Id == 1).FirstOrDefault();
db.User.Remove(user);
db.SaveChanges();
}</code></pre>
<blockquote>注: <strong>Remove</strong> 用于删除单个实体, <strong>RemoveRange</strong> 用于删除多个实体。</blockquote>
<ol><li>显式标记,通过调用 <strong>Entry</strong> 方法,示例代码如下:</li></ol>
<pre><code class="csharp">using (var db = new EFDbContext())
{
var user = db.User..Where(p => p.Id == 1).FirstOrDefault();
db.Entry(user).State = EntityState.Deleted;
db.SaveChanges();
}</code></pre>
<h4>四、Modified</h4>
<p>当我们修改数据时,需要用到 <strong>Modified</strong> 状态,当调用 <strong>SaveChanges</strong> 方法时数据将会修改数据库中的数据。在EF中修改数据,只有一种方法,通过调用 <strong>Entry</strong> 方法,示例代码如下:</p>
<pre><code class="csharp">using (var db = new EFDbContext())
{
var user = db.User..Where(p => p.Id == 1).FirstOrDefault();
db.Entry(user).State = EntityState.Modified;
db.SaveChanges();
}</code></pre>
<h4>五、特别提示</h4>
<p>所有状态之间几乎都可以通过 <strong>Entry(T).State</strong> 的方式进行强制状态转换,因为状态改变都是依赖于 Id 的( Added 除外)。我们通过下图来了解一下上面五种状态的相互转换:<br><img src="/img/remote/1460000019906248?w=514&h=325" alt="elz3dg.png" title="elz3dg.png"></p>
Entity Framework 多对多映射
https://segmentfault.com/a/1190000019906019
2019-07-29T15:16:23+08:00
2019-07-29T15:16:23+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>上一篇文章我们讲解了EF中的一对对多的关系映射,这篇文章我们讲解EF中的多对多(Many-to-Many Relationship)关系映射。这篇文章我们同样通过一个简单的例子来讲解多对多的关系映射。</p>
<h4>零、自动生成关系表</h4>
<p>故事:在一个学生选课系统中,存在学生和课程两个实体,他们之间的关系是:一个学生可以选择多门课程,一门课程也可以被多个学生选择。</p>
<p>通过上面简单的描述,我们可以分析出学生和课程是多对多的关系。这种关系应设在数据库中就需要第三张表来辅助维持。这个第三张表被称为关联表或链接表,这张表中存存储了学生和课程的主键(或被能够区分唯一性的字段)。现在我们看一下,通过代码怎么来表示多对多关系:</p>
<pre><code class="csharp">//学生类
public class Student:BaseEntity
{
public string Name { get; set; }
public int Age { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
//课程类
public class Course : BaseEntity
{
public string Name { get; set; }
public string TeacherName { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
//基础类
public class BaseEntity
{
public int Id { get; set; }
public DateTime CreateDateTime { get; set; }
}</code></pre>
<p>同上一篇文章一样,我们创建 <strong>Student</strong> 和 <strong>Course</strong> 的映射类</p>
<pre><code class="csharp">//学生映射类
public class StudentsMap : EntityTypeConfiguration<Student>
{
public StudentsMap()
{
//表名称
ToTable("Students");
//主键
HasKey(p => p.Id);
//设置主键自增长
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
//设置要映射的数据
Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(p => p.Age);
Property(p => p.CreateDateTime);
//设置关系
HasMany(p => p.Courses)
.WithMany(p => p.Students)
.Map(p => p.ToTable("StudentCourses")
.MapLeftKey("StudentId")
.MapRightKey("CourseId"));
}
}
//课程映射类
public class CourseMap : EntityTypeConfiguration<Course>
{
public CourseMap()
{
ToTable("Coureses");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50);
Property(p => p.TeacherName);
Property(p => p.CreateDateTime);
}
}</code></pre>
<p>从上面的 <strong><em>学生映射类</em></strong> 可以看出,一个学生可以选择多门课程,一个课程可以被多名学生选择。我们为了实现学生和课程多对多的关系,于是定义了关联表,并且设置了这个关联表中两个外键的名称。</p>
<blockquote>注:</blockquote>
<ol>
<li>在设置多对多关系的时候,如果不定义 <strong>MapLeftKey</strong> 和 <strong>MapRightKey</strong> EF将默认使用 <strong>实体类型_id</strong> 。在本例中如果不定义这两个键的名称的话,EF默认使用的名称是 <strong>Student_Id</strong> 和 <strong>Courses_Id</strong>;</li>
<li>MapLeftKey 是关系键</li>
</ol>
<p>下面我们编写一段代码来测试一下数据库生成的是否是多对多的关系:</p>
<pre><code class="csharp">static void Main(string[] args)
{
using (var efContext = new EFContext())
{
Student student = new Student
{
Name = "张三",
Age = 26,
CreateDateTime = DateTime.Now,
Courses = new List<Course>
{
new Course
{
Name="语文",
TeacherName="王老师",
CreateDateTime=DateTime.Now
},
new Course
{
Name="数学",
TeacherName="孙老师",
CreateDateTime=DateTime.Now
}
}
};
Course course = new Course
{
Name = "英语",
TeacherName = "Jack",
CreateDateTime = DateTime.Now,
Students = new List<Student>
{
new Student
{
Name="王五",
Age=27,
CreateDateTime=DateTime.Now
},
new Student
{
Name="孙琦",
Age=27,
CreateDateTime=DateTime.Now
}
}
};
efContext.Students.Add(student);
efContext.Courses.Add(course);
efContext.SaveChanges();
}
}</code></pre>
<p>代码运行后,数据库将出现三张表 <strong>Students</strong> 、 <strong>Coureses</strong> 和 <strong>StudentCourses</strong> ,其中 <strong>StudentCourses</strong> 是 <strong>关联表</strong> ,该表中将出现 <strong>Students</strong> 和 <strong>Coureses</strong> 之间的关系</p>
<p><img src="/img/remote/1460000019906022?w=845&h=306" alt="ZITyM6.png" title="ZITyM6.png"></p>
Entity Framework 一对一关系映射
https://segmentfault.com/a/1190000019905992
2019-07-29T15:14:48+08:00
2019-07-29T15:14:48+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>一对一关系是Entity Framework 中很复杂的关系,涉及了 <strong>HasOptional</strong> 、<strong>WithRequired</strong> 、 <strong>WithOptionalPrincipal</strong> 、 <strong>WithOptionalDependent</strong>。这篇文章我们将具体讲解这几个的用法。</p>
<p>我们以会员和订单为例,一个会员有可能有订单,也可能没有订单,但是一个订单绝对属于一个会员。我们先编写出会员和订单的类代码:</p>
<pre><code class="csharp">public class Member
{
public int Id { get; set; }
public string Name { get; set; }
public virtual Order Order { get; set; }
}
public class Order
{
public int Id { get; set; }
public int Name { get; set; }
public int Num { get; set; }
public virtual Member Member { get; set; }
}
</code></pre>
<h4>零、 HasOptionl then WithRequired</h4>
<p>这种方式的会员和订单的映射类如下:</p>
<pre><code class="csharp">public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Member");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasOptional(p => p.Order).WithRequired(p => p.Member);
}
}
public class OrderMap : EntityTypeConfiguration<Order>
{
public OrderMap()
{
ToTable("Order");
HasKey(p => p.Id);
Property(p => p.Id).HasColumnName("MemberId")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}</code></pre>
<p>这里将 <strong>Member</strong> 类的Id设为自增长,将 <strong>Order</strong> 的Id设置别名 <strong>MemberId</strong> 且非自增长。编写晚上下文类和调用类后,运行代码后,我们在数据库中将看到如下图:</p>
<p><img src="/img/remote/1460000019905995?w=656&h=671" alt="Zz7Ade.png" title="Zz7Ade.png"></p>
<p>其中 <strong>MemberId</strong> 就是在 OrderMap 中设置的别名</p>
<h4>一、 HasOptionl then WithOptionalPrincipal</h4>
<p>现在我们修改一下 <strong>MemberMap</strong> 和 <strong>OrderMap</strong> ,将 <strong>Member</strong> 和 <strong>Order</strong> 的Id都设置成自增长。</p>
<pre><code class="csharp">public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Member");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasOptional(p => p.Order).WithOptionalPrincipal(p => p.Member);
}
}
public class OrderMap : EntityTypeConfiguration<Order>
{
public OrderMap()
{
ToTable("Order");
HasKey(p => p.Id);
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}</code></pre>
<p>再次执行代码后,查看生成的数据库:</p>
<p><img src="/img/remote/1460000019905996" alt="ZzbZvt.png" title="ZzbZvt.png"></p>
<p>我们看到 <strong>Order</strong> 表中 <strong>Member_Id</strong> 字段是EF自动生成的外键,且不可为空</p>
<h4>二、 HasOptionl then WithOptionalDependent</h4>
<p>再次修改 <strong>MemberMap</strong> 和 <strong>OrderMap</strong> :</p>
<pre><code class="csharp">public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Member");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasOptional(p => p.Order).WithOptionalDependent(p => p.Member);
}
}
public class OrderMap : EntityTypeConfiguration<Order>
{
public OrderMap()
{
ToTable("Order");
HasKey(p => p.Id);
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}</code></pre>
<p>保存代码,运行程序,查看数据库:</p>
<p><img src="/img/remote/1460000019905997?w=532&h=586" alt="ZzqDw8.png" title="ZzqDw8.png"></p>
<p>和上一小节生成的数据库相比,这一小节生成的数据库 <strong>Member</strong> 表中自动生成了 <strong>Order</strong> 表的外键 <strong>Order_Id</strong> ,而 <strong>Order</strong> 表没有生成任何外键。</p>
<blockquote>注:使用 <strong>WithOptionalPrincipal</strong> 可以使实体作为主体,将包含关系主键。使用 <strong>WithOptionalDependent</strong> 可以使实体作为以来提,将包含关系的外键。</blockquote>
<p>前面所讲的都是从 <strong>Member</strong> 入手,我们同样也可以从 <strong>Order</strong> 表入手,但是在实际开发中我不建议这么做。下面就来说一下从 <strong>Order</strong> 入手的方法。</p>
<h4>三、 HasRequired then WithOptional</h4>
<p>同样我们修改 <strong>MemberMap</strong> 和 <strong>OrderMap</strong> :</p>
<pre><code class="csharp">public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Member");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
public OrderMap()
{
ToTable("Order");
HasKey(p => p.Id);
Property(p => p.Id).HasColumnName("MemberId").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(p => p.Member).WithOptional(p => p.Order);
}</code></pre>
<p>这种方法生成的数据库与第一种方法结果一样。</p>
<h4>四、 HasRequired 和 WithOptional</h4>
<p>我们最后一次修改 <strong>MemberMap</strong> 和 <strong>OrderMap</strong> :</p>
<pre><code class="csharp">public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Member");
HasKey(p => p.Id);
Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
public class OrderMap : EntityTypeConfiguration<Order>
{
public OrderMap()
{
ToTable("Order");
HasKey(p => p.Id);
Property(p => p.Id).HasColumnName("MemberId").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(p => p.Member).WithOptional(p => p.Order).Map(p => p.MapKey("Member_Id")))
}
}
</code></pre>
<p>这种方法生成的数据库与第二种方法结果一样。</p>
控制反转_依赖注入简明教程
https://segmentfault.com/a/1190000019629149
2019-07-01T12:43:28+08:00
2019-07-01T12:43:28+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>在面向对象中IOC是一个重要的设计思想。这篇文章将带领大家快速掌握控制反转和依赖注入。</p>
<blockquote>注:代码基于c#</blockquote>
<h4>零、Ioc</h4>
<p>Ioc 英文是 Inversion of Control,中文是控制反转。所谓控制反转,就是A类中有对B类方法的调用,我们调用之前一般都会先new,这样就增加了类和类之间的耦合度。为了降低耦合度,将A类对B类的的控制权交给Ioc容器,让双方都依赖Ioc容器。</p>
<h4>一、DI</h4>
<p>DI 的英文是 Dependency Injection,中文是依赖注入。依赖注入是实现Ioc的一种方式,也是常用的方式。依赖注入的方式主要有三种:<strong><em>构造函数注入</em></strong>、<strong><em>接口注入</em></strong> 和 <strong><em>属性注入</em></strong>。(因为这篇文章知识一个简单的入门,因此我们不讲解这三种注入)我们来通过一个例子,来看一下依赖注入的好处:</p>
<p>故事:小吴是一个公司的CEO,每天都需要司机开车送他上下班,开始他只有一个司机,每次司机生病,他就只能自己开车上下班。因此小吴设立了一个司机部门,部门中有多名司机,由司机部门给小吴指派司机。</p>
<p>分析:从上面的故事可以分析得出,刚开始小吴是依赖者,司机是被依赖者,小吴依赖于小刚。后来通过增加司机部门这个Ioc容器,<br>小吴和小刚之间的关系变为了,小吴依赖于司机部门。</p>
<p>我们通过代码看一下(这里使用到了 .NET 依赖注入容器 <strong><em>AutoFac</em></strong>):</p>
<pre><code class="csharp">static void Main(string[] args)
{
//接小吴
IContainer driverCont = DriverDepartment();
//司机部门分配一个司机给CEO小吴
CE0_Wu wu = driverCont.Resolve<CE0_Wu>();
wu.Car();
Console.Read();
}
/// <summary>
/// 司机部门
/// </summary>
/// <returns></returns>
private static IContainer DriverDepartment()
{
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<CE0_Wu>();
builder.RegisterType<Driver>().As<IDriver>();
return builder.Build();
}
}
/// <summary>
/// 抽象以来
/// </summary>
public class Driver : IDriver
{
/// <summary>
/// 开车
/// </summary>
public void Drive()
{
Console.WriteLine("开车送老板");
}
}
public interface IDriver
{
void Drive();
}
/// <summary>
/// 小吴
/// </summary>
public class CE0_Wu
{
private IDriver driver;
public CE0_Wu(IDriver driver)
{
this.driver = driver;
}
public void Car()
{
driver.Drive();
}
}</code></pre>
Entity Framework 一对多关系映射
https://segmentfault.com/a/1190000019629114
2019-07-01T12:39:06+08:00
2019-07-01T12:39:06+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>EF中关系映射也是一个很关键的内容,关系映射和属性映射一样,也是在 <strong>OnModelCreating</strong> 中配置映射。EF中的关系映射有如下三种:</p>
<ol>
<li>One-to-Many Relationship(一对多)</li>
<li>Many-to-Many Relationship(多对多)</li>
<li>One-to-One Relationship(一对一)</li>
</ol>
<p>我们今天先讲解 <strong>One-to-Many Relationship(一对一关系)</strong></p>
<h4>零、创建所需类</h4>
<ul><li>所有实体类公用的抽象基类</li></ul>
<pre><code class="csharp">public abstract class Base
{
public int Id { get; set; }
public DateTime CreateTime { get; set; }
public DateTime ModifiedTime { get; set; }
}</code></pre>
<ul><li>客户类和订单类</li></ul>
<pre><code class="csharp">public class Customer : Base
{
public string Name { get; set; }
public string Email { get; set; }
public virtual ICollection<Order> Orders { get; set; }
}
public class Order : Base
{
public byte Quanatity { get; set; }
public int Price { get; set; }
public int CoustomerId { get; set; }
public virtual Customer Customer { get; set; }
}</code></pre>
<h4>一、One-to-Many Relationship</h4>
<ul><li>创建Map映射类</li></ul>
<p>在编写代码之前,我们先分析一下客户和订单的关系。一个客户可以有多个订单,但一个订单只能属于一个客户,所以我们用到了EF中的 <strong><em>HasRequired</em></strong>,一个客户又存在多个订单,因此也使用到了 <strong><em>WithMany</em></strong> ,同时 <strong>Order</strong> 表中有 <strong>CustomerId</strong> 作为外键,因此我们用到了 <strong><em>HasForeignKey</em></strong> 。根据我们的分析,编写代码如下:</p>
<pre><code class="csharp">public class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
//数据库映射的表名称
ToTable("Customer");
//主键
HasKey(p => p.Id);
//属性映射的字段属性
Property(p => p.Name).HasColumnType("VARCHAR").HasMaxLength(50).IsRequired();
Property(p => p.Email).HasColumnType("VARCHAR").HasMaxLength(50).IsRequired();
Property(p => p.CreateTime);
Property(p => p.ModifiedTime);
//设置关系
HasMany(t => t.Orders).WithRequired(t => t.Customer).HasForeignKey(t => t.CoustomerId).WillCascadeOnDelete(false);
}
}
public class OrderMap : EntityTypeConfiguration<Order>
{
public OrderMap()
{
ToTable("Order");
HasKey(p => p.Id);
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(p => p.Quanatity);
Property(p => p.Price);
Property(p => p.CoustomerId);
Property(p => p.CreateTime);
Property(p => p.ModifiedTime);
}
}</code></pre>
<ul><li>注册映射类</li></ul>
<pre><code class="csharp">protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var typeToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => !String.IsNullOrEmpty(t.Namespace))
.Where(t => t.BaseType != null
&& t.BaseType.IsGenericType
&& t.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach(var type in typeToRegister)
{
dynamic configurationInstance = Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
base.OnModelCreating(modelBuilder);
}</code></pre>
<blockquote>注1:在实际项目中需要编写很多的实体类,如果将所有实体类的映射直接写在 <strong>OnModelCreating</strong> 中会造成代码臃肿,不易维护,因此我们在这里将每个类的映射写在对应的映射文件中,最后再将每个类的映射类注册到 <strong>OnModelCreating</strong> 中<p>注2:上述代码和描述是从客户的方向连编写的关系映射,如果以订单的角度来编写关系映射的话,只需删掉CustomerMap中的关系配置,在OrderMap中增加关系配置部分修改如下:</p>
</blockquote>
<pre><code class="csharp">HasRequired(p => p.Customer).WithMany(p => p.Orders).HasForeignKey(p => p.CoustomerId).WillCascadeOnDelete(false);</code></pre>
<p>运行控制台代码后,我们将在数据库看到表和表关系都被创建了:</p>
<p><img src="/img/remote/1460000019629117?w=467&h=441" alt="Z1xRKA.png" title="Z1xRKA.png"></p>
C# 三个Timer
https://segmentfault.com/a/1190000019629090
2019-07-01T12:36:56+08:00
2019-07-01T12:36:56+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>在C#中存在3种常用的 <strong><em>Timer</em></strong> :</p>
<ol>
<li>System.Windows.Forms.Timer</li>
<li>System.Timers.Timer</li>
<li>System.Threading.Timer</li>
</ol>
<h4>零、System.Windows.Forms.Timer</h4>
<p>这个 <strong>Timer</strong> 是单线程的,也就是说只要它运行,其他线程就要等着。<br><img src="/img/remote/1460000019629093?w=240&h=240" alt="ZElKBR.gif" title="ZElKBR.gif"></p>
<p>这个 Timer 有如下特点:</p>
<ol>
<li>完全基于UI线程,定时器触发时,操作系统把定时器消息插入线程消息队列中,调用线程执行一个消息泵提取消息,然后发送到回调方法Tick中;</li>
<li>使用 <strong><em>Start</em></strong> 和 <strong><em>Stop</em></strong> 启动和停止 Timer;</li>
<li>UI操作过长会导致 Tick 丢失;</li>
<li>可以使用委托Hook Tick事件;</li>
<li>精确度不高;</li>
<li>通过将 <strong>Enabled</strong> 设置为 <strong>True</strong>,使 Timer 自动运行</li>
</ol>
<p>从上面的第一个特点可以得知,该 Timer 会造成 WinForm UI 假死,因此如果需要定时处理大量计算或者大量IO操作的任务,不建议使用该 Timer ,接下来我们看一个例子体会一下在IO操作的情况下出现的假死情况:</p>
<p>我们在Form中放入两个Button 一个Lable和一个Timer<br><img src="/img/remote/1460000019629094?w=828&h=430" alt="ZetMuV.png" title="ZetMuV.png"></p>
<pre><code class="csharp">private void Button_Click(object sender, EventArgs e)
{
timer.Interval = 1000;
timer.Tick += Timer_Tick;
timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
for (int i = 0; i < 10000; i++)
{
File.AppendAllText(Directory.GetCurrentDirectory()+"test.txt", i.ToString());
this.label_output.Text = "当前操作:插入数字" + i;
}
}</code></pre>
<p>我们单击计算按钮,我们会发现WinForm出现了假死(无法移动窗口、按钮无法点击等)</p>
<h4>一、System.Timers.Timer</h4>
<p>该 Timer 是基于服务器的计时器,是为在多线程环境中用于辅助线程而设计的,可以在线程间移动来处理引发的 Elapsed 事件,比上一个计时器更加精确。</p>
<p>该 Timer 有如下特点:</p>
<ol>
<li>通过 <strong>Elapsed</strong> 设置回掉处理事件,且 <strong>Elapsed</strong> 是运行在 <strong>ThreadPool</strong> 上的;</li>
<li>通过 <strong>Interval</strong> 设置间隔时间;</li>
<li>当 <strong>AutoReset</strong> 设置为 <strong>False</strong> 时,只在到达第一次时间间隔后触发 <strong>Elapsed</strong> 事件;</li>
<li>是一个多线程计时器;</li>
<li>无法直接调用 WinForm 上的控件,需要使用 <strong>委托</strong>;</li>
<li>主要用在 Windows 服务中。</li>
</ol>
<p>同样我们通过代码来看一下该 Timer 计时器怎么使用:</p>
<pre><code class="csharp">System.Timers.Timer timersTimer = new System.Timers.Timer();
private void Button_Click(object sender, EventArgs e)
{
timersTimer.Interval = 1000;
timersTimer.Enabled = true;
timersTimer.Elapsed += TimersTimer_Elapsed;
timersTimer.Start();
}
private void TimersTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
for (int i = 0; i < 10000; i++)
{
this.BeginInvoke(new Action(() =>
{
this.label_output.Text="当前时间:"+DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}), null);
}
}
private void Button1_Click(object sender, EventArgs e)
{
timersTimer.Stop();
}</code></pre>
<p>运行上面代码,会发现WinForm界面假死的情况消失了。</p>
<h4>二、System.Threading.Timer</h4>
<p>该 Timer 同样也是一个多线程的计时器,它有如下特点:</p>
<ol>
<li>多线程</li>
<li>和前两个计时器相比没有 <strong>Start</strong> 和 <strong>Stop</strong> 方法,如果要停止计时器,必须调用 <strong>Dispose</strong> 方法来销毁 Timer 对象;</li>
<li>调用 <strong>Dispose</strong> 方法后并不能马上停止所有的计时器,这是因为间隔时间小于执行时间时多个线程运行造成的,多个线程无法同时停止;</li>
</ol>
<p><img src="/img/remote/1460000019629095" alt="ZeDVl4.gif" title="ZeDVl4.gif"></p>
<ol>
<li>是一个轻量级的计时器;</li>
<li>所有的参数全部在构造函数中进行了设置;</li>
<li>可以设置启动时间;</li>
<li>不建议再 WinForm 程序中使用。</li>
</ol>
<p>我们来看一下代码(在控制台应用程序中输入以下代码):</p>
<pre><code class="csharp">static System.Threading.Timer threadingTimer;
static int numSum = 0;
static void Main(string[] args)
{
threadingTimer = new System.Threading.Timer(new System.Threading.TimerCallback(threadingTimer_Elapsed), null, 0, 1000);
Console.Read();
}
private static void threadingTimer_Elapsed(object state)
{
for (int i = 0; i < 10000; i++)
{
numSum++;
Console.WriteLine("输出数字:"+i);
}
if (numSum > 10000)
{
threadingTimer.Dispose();
Console.WriteLine("结束");
}
}</code></pre>
<blockquote>注意:当我们不再需要多线程Timer计时器的时候,我们可以调用 <strong>Dispose</strong> 方法释放所占有的资源。但是因为Timer计时器是按线程池线程来安排回调执行的,因此回调可能发生在 Dispose方法的重载被调用之后,所以我们可以使用可使用 Dispose(WaitHandle) 方法等待所有回掉完成。</blockquote>
Entity Framework 小知识(四)
https://segmentfault.com/a/1190000019509868
2019-06-18T11:14:07+08:00
2019-06-18T11:14:07+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>在EF中并没有提供包含索引和过滤索引的创建方法,那么我们就么发创建了吗?答案是否定的,我们可以通过迁移类进行创建包含索引和过滤索引。<br>首先我们通过 <strong>Add-Migration</strong> 命令创建一个空的迁移类,然后在 <strong>Up</strong>方法中输入如下代码:</p>
<pre><code class="csharp">Sql($"CREATE NONCLUSTERED INDEX [{IndexName}] ON [dbo].[User]([Name] INCLUDE ([IdNumber]))");
</code></pre>
<p>在 <strong>Down</strong> 方法中输入如下代码:</p>
<pre><code class="csharp">DropIndex("dbo.User","IndexName")</code></pre>
Entity Framework 索引
https://segmentfault.com/a/1190000019499901
2019-06-17T12:56:37+08:00
2019-06-17T12:56:37+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>Entity Framwework 6 设置和使用索引,是一个比较 egg 疼的事情,为什么这么说呢?因为Entity Framwework 6的不同版本有不同的设置和使用方法,按照版本来划分,有三种方法:</p>
<ol>
<li>EF6 方法</li>
<li>EF6.1.x方法</li>
<li>EF6.2.x方法</li>
</ol>
<h4>EF6</h4>
<p>EF6中设置索引比较麻烦,我们需要先进行code first 迁移,然后在迁移类中的 <strong>Up</strong> 方法中输入如下代码:</p>
<pre><code class="csharp">//创建索引且值唯一
CreateIndex("dbo.User","Name",unique:true);
//创建复合索引,索引名称为 **NameAndIdNumber**
CreateIndex("dbo.User",new []{"Name","IdNumber"},name:"NameAndIdNumber");</code></pre>
<p>在 <strong>Down</strong> 方法中输入如下代码:</p>
<pre><code class="csharp">DropIndex("dbo.User","Name");
DropIndex("dbo.User",new []{"Name","IdNumber"});</code></pre>
<blockquote>注:EF6中通过迁移类创建的索引无法重命名</blockquote>
<h4>EF6.1.x</h4>
<p>该版本定义索引的方法如下:</p>
<pre><code class="csharp">public virtual void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(p => p.Name).HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()
{
IsUnique=true
}));
}</code></pre>
<p>上面这段代码的意思是,给User表创建一个唯一索引Name。同样上面的代码也可以单独定义在一个类中:</p>
<pre><code class="csharp">public class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
Property(p => p.Name).HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute() {
IsUnique=true
}));
}
}</code></pre>
<p>我们前面知道在EF6中创建的索引无法重命名,那么在EF6.1.x中创建的索引是否可以重命名吗?答案是当然可以,我们只需在前一类中的 <strong>Up</strong> 和 <strong>Down</strong> 方法写入如下代码即可:</p>
<pre><code class="csharp">public override void Up()
{
RenameIndex(table:"db.User",name:"Name",newName:"NameIndex");
}
public override void Down()
{
RenameIndex(table:"db.User",name:"NameIndex",newName:"Name");
}
</code></pre>
<h4>EF6.2.x</h4>
<p>在EF6.2.X中创建索引比较简单,只需要调用 <strong>HasIndex</strong> 方法即可。</p>
<pre><code class="csharp">public class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
HasIndex(p=>p.Name);
//创建复合索引
HasIndex(p=>new {
Name=p.Name,
IdNumber=p.IdNumber
});
}
}</code></pre>
Entity Framework 小知识(三)
https://segmentfault.com/a/1190000019476284
2019-06-14T09:59:40+08:00
2019-06-14T09:59:40+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、乐观并发</h4>
<p>在单服务器上运行的站点,为了防止出现脏读现象,我们一般使用Lock语句关键字,但是如果在分布式站点上使用Lock语句关键字是不起作用的,因为程序锁住了服务器1数据库实例,但服务器2并不知道服务器1已被锁住,这样依然会出现脏读现象。这时我们就用到了EF的乐观并发。</p>
<p>EF中解决并发有两种方式:</p>
<ol>
<li>利用并发Token;</li>
<li>利用行版本的方式</li>
</ol>
<p>代码如下:</p>
<pre><code class="csharp">public class EfDbContext : DbContext
{
public EfDbContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>());
}
public virtual void OnModelCreating(DbModelBuilder modelBuilder)
{
// 利用并发Token
modelBuilder.Entity<Users>().Property(t=>t.Name).IsConcurrencyToken();
// 利用行版本
modelBuilder.Entity<Users>().Property(t=>t.Name).IsRowVersion();
}
}</code></pre>
<blockquote>注:在并发量不是很大的时候可以使用EF的乐观并发,在访问量很大的时候应该使用其他技术处理并发问题。</blockquote>
Entity Framework复杂类型属性映射
https://segmentfault.com/a/1190000019465875
2019-06-13T11:22:36+08:00
2019-06-13T11:22:36+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、创建项目必须代码</h4>
<pre><code class="csharp">public class BaseModel
{
public int Id { get; set; }
public DateTime CreateDateTime { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
public class User:BaseModel
{
public string Name {get;set;}
public string Birthdate {get;set;}
public string IdNumber {get;set;}
public Address Address {get;set;}
}</code></pre>
<p>以上代码在ORM中称为组合类,EF会将这两个类映射在一张表中。当Code First发现不能推断出类的主键,并且没有通过Data Annotations或Fluent API注册主键,那么该类型将被自动注册为复杂类型。</p>
<blockquote>注意:</blockquote>
<ol>
<li>复杂类型检测要求该类型不具有引用实体类型的属性,还要求不可引用另一类型的集合属性</li>
<li>复杂类型的在数据库中映射的列名称为:负载类型类名_属性名</li>
</ol>
<p>我们接下来创建 <strong>DbContext</strong> 类</p>
<pre><code class="csharp">public class EfDbContext : DbContext
{
public EfDbContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>());
}
public DbSet<User> Users { get; set; }
}</code></pre>
<p>创建完DbContext类后,我们编写将数据存入数据库的方法:</p>
<pre><code class="csharp">
using (var efDbContext = new EfDbContext())
{
var user = new User()
{
Birthdate = DateTime.Now,
CreateDateTime = DateTime.Now,
Name = "张三",
IdNumber = "1234567"
};
efDbContext.Users.Add(user);
efDbContext.SaveChanges();
}
</code></pre>
<p>运行上述代码,会得到如下错误:<br><img src="/img/remote/1460000019465878?w=719&h=450" alt="VyO83V.png" title="VyO83V.png"></p>
<p>出现上述错误的原因是我们没有初始化 <strong>Address</strong> 类,其中一个(后面我会讲解另一个解决方法)解决方法是在 <strong>new User(){}</strong> 内初始化 <strong>Address</strong>,修正后的代码如下:</p>
<pre><code class="csharp">using (var efDbContext = new EfDbContext())
{
var user = new User()
{
Birthdate = DateTime.Now,
CreateDateTime = DateTime.Now,
Name = "张三",
IdNumber = "1234567",
Address = new Address()
};
efDbContext.Users.Add(user);
efDbContext.SaveChanges();
}</code></pre>
<h4>一、如何正确使用复杂类型</h4>
<ol>
<li>为避免添加实体报错,应该在实体的构造函数中初始化复杂类型;</li>
<li>将制度属性添加到复杂类型中时,需进行空值检查;</li>
<li>尽量显式注册复杂类型。</li>
</ol>
<p>现在我们按照上面所述,对我们先前编写的内容进行改造,这三条规则也是解决我们前面所遇到的BUG的另一个方法。</p>
<pre><code class="csharp">public class BaseModel
{
public int Id { get; set; }
public DateTime CreateDateTime { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public bool HasValue
{
get
{
return (Street != null || ZipCode != null || City != null);
}
}
}
public class User : BaseModel
{
public User()
{
Address = new Address();
}
public string Name { get; set; }
public DateTime Birthdate { get; set; }
public string IdNumber { get; set; }
public Address Address { get; set; }
}
public class EfDbContext : DbContext
{
public EfDbContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>());
}
public virtual void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.ComplexType<Address>();
}
public DbSet<User> Users { get; set; }
}
</code></pre>
<p>代码改造后我们可以轻松的通过 <strong><em>变更追踪API</em></strong> 来访问数据的原始值和当前值。所谓原始值就是从数据库查询出来的值,当前值就是实体目前的值。入口点是 <strong><em>DbContext的Entry方法</em></strong>,返回对象类型是 <strong><em>DbEntityEntry</em></strong> 。我们看一下访问原始值和当前值得例子:</p>
<pre><code class="csharp">using (var efDbContext = new EfDbContext())
{
var user = efDbContext.Users.Find(1);
var oriValue = efDbContext.Entry(user).ComplexProperty(u => u.Address).OriginalValue;
//将city的值改为北京
user.Address.City = "北京";
var curValue = efDbContext.Entry(user).ComplexProperty(u => u.Address).CurrentValue;
Console.WriteLine("原始值:"+oriValue.City+" 当前值:"+curValue.City);
Console.Read();
}
</code></pre>
<p>运行上述代码,将会看到如下的输出:<br><img src="/img/remote/1460000019465879?w=1488&h=776" alt="V2VUKS.png" title="V2VUKS.png"></p>
<p>同样,我们也可以通过链式调用,获取复杂了类型的属性或者设置复杂类型的属性:</p>
<pre><code class="csharp">var user = efDbContext.Users.Find(1);
var city = efDbContext.Entry(user).ComplexProperty(u => u.Address).Property(a => a.City).CurrentValue;
Console.Write(city);</code></pre>
<h4>二、复杂类型的限制</h4>
<p>从上面的讲解我们卡一看到,用复杂类型很双,一直用一直爽,但是复杂类型还是有他的限制的:</p>
<ol>
<li>不能共享引用:因为没有主键标识,不能被自身实例之外的任何对象引用;</li>
<li>没有优雅的方式标识空引用:即使查询出的数据为空,EF Code First 依然会初始化复杂类型对象;</li>
<li>无法延迟加载。</li>
</ol>
Entity Framewor简单属性映射
https://segmentfault.com/a/1190000019465830
2019-06-13T11:19:52+08:00
2019-06-13T11:19:52+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>本节我们只介绍在EF中比较常见的映射</p>
<h4>零、表名映射</h4>
<p>默认情况下可以不配置表名,我们的模型名称将会作为数据库的表名。但是大部分项目会要求数据库表名称的规范,例如我们要将模型 <strong>User</strong> 在数据库中映射为 <strong>Users</strong>,那么我们可以这么做,在派生类上下文中的 <strong><em>OnModelCreating</em></strong> 中进行如下定义:</p>
<pre><code class="csharp">modelBuilder.Entity<User>().ToTbale("Users");</code></pre>
<h4>一、主键映射</h4>
<p>表的主键我们一般习惯使用 <strong>Id</strong> 或者以 <strong>Id</strong> 结尾的方式来命名,EF默认情况下会将 <strong>Id</strong> 或以 <strong>Id</strong> 结尾的属性作为主键,如果两者都存在的话,默认会以 <strong>Id</strong> 作为主键。但是,还存在如下几种情况:</p>
<ol>
<li>设置联合主键;</li>
<li>主键为 <strong>int</strong> 类型,但是不是自增长的,而是手动分配的。</li>
</ol>
<p>针对上面两种情况,我们分别进行如下配置:</p>
<pre><code class="csharp">//设置联合主键
modelBuilder.Entity<User>().HasKey(k => new
{
Id=k.Id,
UserId=k.UserId
});
//手动分配主键值
modelBuilder.Entity<User>().HasKey(k => k.Id).Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);</code></pre>
<p>DatabaseGeneratedOption 是枚举类型,值如下:</p>
<table>
<thead><tr>
<th>值</th>
<th>说明</th>
</tr></thead>
<tbody>
<tr>
<td>Identity</td>
<td>标识列</td>
</tr>
<tr>
<td>Computed</td>
<td>计算列</td>
</tr>
<tr>
<td>None</td>
<td>手动分配值</td>
</tr>
</tbody>
</table>
<h4>二、数值映射</h4>
<p>数据库中的数值类型有很多种,C#中也有很多数值类型,但是我们无法直接将C#中的数值类型转换为数据库中的数值类型。那么怎么将C#数值类型映射为数据库数值类型呢?这里我们以 C# <strong>float</strong> 为例,来看一下代码:</p>
<pre><code class="csharp">modelBuilder.Entity<User>().Property(p=>p.Float);</code></pre>
<p>通过上面的代码,我们将 C# <strong>float</strong> 类型映射为了数据库的 <strong>real</strong> 类型。下表是C#数值类型对应的数据库的数值类型:</p>
<table>
<thead><tr>
<th>C#数值类型</th>
<th>数据库数值类型</th>
</tr></thead>
<tbody>
<tr>
<td>int</td>
<td>int</td>
</tr>
<tr>
<td>double</td>
<td>float</td>
</tr>
<tr>
<td>float</td>
<td>real</td>
</tr>
<tr>
<td>decimal</td>
<td>decimal(18,2)</td>
</tr>
<tr>
<td>Int64</td>
<td>bigint</td>
</tr>
</tbody>
</table>
<p>我们看到上表中有一个C#数值类型 <strong>decimal</strong> 对应的数据库数值类型是 <strong>decimal(18,2)</strong> ,括号中的2代表小数点后保留2位,但是在一些情况下我们需要保留小数点后面N位,这时我们可以这么做:</p>
<pre><code class="csharp">modelBuilder.Entity<User>().Property(p=>p.Money).HasPrecision(18,4);</code></pre>
<h4>三、字符串映射</h4>
<p>当我们未对string类型的属性配置映射时,默认的数据库类型是 <strong>nvarchar(max)</strong>,但是大部分情况下不会使用这个默认的映射。举几个例子来讲解一下怎么来改变这个默认映射。</p>
<ol><li>字段不可为空</li></ol>
<pre><code class="csharp">//设置Name属性在数据库映射不可为空
modelBuilder.Entity<User>().Property(p=>p.Name).IsRequired();</code></pre>
<ol><li>字段可为空</li></ol>
<pre><code class="csharp">//设置Birthday属性在数据库映射可为空
modelBuilder.Entity<User>().Property(p=>p.Birthday).IsOptional();</code></pre>
<h4>四、日期映射</h4>
<p>EF中的日期类型在数据库中默认映射为Date,但是数据库中的日期类型还有很多,并且有时候我们需要将日期类型映射为数据库其他类型,那么我们该怎么做呢?这里我们以映射为 <strong>DateTime</strong> 为例:</p>
<pre><code class="csharp">modelBuilder.Entity<User>().Property(p=>p.CreateDateTime).HasColumnType("DATETIME");</code></pre>
<blockquote>注:数值类型和日期类型属于值类型,因此我们不需要通过 <strong>IsRequired</strong> 来配置映射字段不可为空,因为默认就是不为空的。但是可以通过 <strong>IsOptional</strong> 设置可为空。</blockquote>
Entity Framework 约定
https://segmentfault.com/a/1190000019465732
2019-06-13T11:14:18+08:00
2019-06-13T11:14:18+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<p>约定,类似于接口,是一个规范和规则,使用Code First 定义约定来配置模型和规则。在这里约定只是记本规则,我们可以通过Data Annotaion或者Fluent API来进一步配置模型。约定的形式有如下几种:</p>
<ul>
<li>类型发现约定</li>
<li>主键约定</li>
<li>关系约定</li>
<li>复杂类型约定</li>
<li>自定义约定</li>
</ul>
<h4>零、类型发现约定</h4>
<p>在Code First 中。我们定义完模型,还需要让EF上下文你知道应该映射那些模型,此时我们需要通过 <strong><em>DbSet</em></strong> 属性来暴露模型的。如果我们定义的模型由继承层次,只需要为基类定义一个DbSet属性即可(如果派生类与基类在同一个程序集,派生类将会被自动包含),代码如下:</p>
<pre><code class="csharp">public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public virtual ICollection<Blog> Blogs { get; set; }
}
public class EfDbContext : DbContext
{
public EfDbContext()
{
}
public DbSet<Department> Departments { get; set; }
}
</code></pre>
<p>当然,有时候我们不希望模型映射到数据库中,这时我们可以通过Fluent API 来忽略指定的模型映射到数据库中,代码写在EF上下文中:</p>
<pre><code class="csharp">protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<Department>();
}</code></pre>
<h4>一、主键约定</h4>
<p>Code First 会根据模型中定义的<strong><em>id</em></strong>,推断属性为主键(如果类中没有id属性,会查找定义成<strong><em>类名称+id</em></strong>的属性,将这个属性作为主键)。如果主键类型是<strong><em>int</em></strong> 或者 <strong><em>guid</em></strong> 类型,主键将会被映射为自增长标识列。例如我们上一小节中定义的类 <strong><em>Department</em></strong>,类中没有名称为id的属性,但是存在名称为类名称+id的属性<strong><em>DepartmentId</em></strong>,因此DepartmentId属性,将会被映射为自增长的主键。如果一个类中既没有id属性,也没有类名+id的属性,那么代码在运行时将会报错,因为EF没有找到符合要求的字段创建主键。</p>
<h4>二、关系约定</h4>
<p>在数据库中,我们可以通过多张表的关联查询出数据,这多张表之间的关联,就是他们的关系。同样,也可以在模型中定义这样的关系。EF中定义关系要使用到导航属性,通过导航属性可以定义多个模型之间的关系。大部分情况下我们会将导航属性和外键属性结合在一起使用。导航属性的命名规则如下:<strong>导航属性名称+主体主键名称</strong> 或者 <strong>主体类名+主键属性名称</strong> 或者 <strong>主体主键属性名</strong>。当EF检测出外键属性后,会根据外键属性是否为空来判断关系,如果外键可以为空,那么模型之间的关系将会配置成可选的,Code First 不会再关系上配置级联删除。看一个简单的代码:</p>
<pre><code class="csharp">public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string Name { get; set; }
public int DepartmentId { get; set; }
public virtual Department Department { get; set; }
}</code></pre>
<h4>三、复杂类型约定</h4>
<p>在Code First 不能推断出模型中的主键,并且没有通过Data Annotations 或者Fluent API进行手动配置主键时,该模型将会自动被配置为复杂类型,检测复杂类型时要求该类型没有引用实体类型的属性。简单的说就是:一个复杂类型作为已存在对象的属性,EF会将复杂类型的类映射到已存在的表中,已存在的表包将包含这些列,而不是将复杂类型映射成另外单独的一张表。我们来看一下例子:</p>
<pre><code class="csharp">public class EfDbContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().ToTable("Orders");
modelBuilder.ComplexType<Order.Address>();
}
public DbSet<Order> Orders { get; set; }
}
public class Order
{
public int Id;
public string Name;
public class Address
{
public string Street;
public string Region;
public string Country;
}
}
</code></pre>
<h4>四、自定义约定</h4>
<p>当EF提供的默认约定都不符合我们要求的时候,我们可以使用自定义约定。自定义约定可以看作全局约定规则,将会运用到所有实体和属性,也可以显示实现应用到指定的模型上。</p>
<p>如果项目要求模型中有Id属性,就将Id作为主键映射,那么我们有两种选择来定义这个约定,首先我们而已选择Fluent API ,其次我们也可以选择自定义约定。自定义约定相对来说比Fluent API 要简单,只需一行代码即可解决。我们只需要在 <strong><em>OnModelCreating</em></strong> 方法中加入如下代码即可:</p>
<pre><code class="csharp">modelBuilder.Properties().Where(p => p.Name == "Id").Configure(p => p.IsKey());</code></pre>
<blockquote>注:当多个属性存在相同约定配置时,最后一个约定将覆盖前面所有相同的约定。</blockquote>
<p>自定义约定包含一个约定接口 <strong><em>IConvention</em></strong>,IConceptualModelConvention 是概念模型接口,在模型创建后被调用,IStoreModelConvention 接口为存储模型接口,在模型创建之后用于操作对模型的存储,<strong><em>自定义类约定</em></strong>都必须在 <strong><em>OnModelCreating</em></strong> 方法中显式配置,例如我们要将模型中类型为DateTime的属性映射为datetime2,可进行如下配置:</p>
<pre><code class="csharp">public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>().Configure(c => c.HasColumnType("datetime2"));
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(new DateTime2Convention());
}
</code></pre>
<p>当我们自定义约定需要在另一个约定运行之前或者运行之后执行时,有可能会受到默认原定的影响,这时我们可以用到:<strong><em>AddBefore<strong> 和 </strong></em></strong>AddAfter* 方法,例如:将我们前面创建的约定放在内置约定发现逐渐约定之前运行。</p>
<pre><code class="csharp">protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
}</code></pre>
<p>在开发过程中都会存在开发规范,例如对表名命名的规则,我们可以调用Types 方法该表表明约定,代码如下:</p>
<pre><code class="csharp">public string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]",m=>m.Value[0]+"_"+m.Value[1]);
return result.ToLower();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Types().Configure(c => c.ToTable(GetTableName(c.ClrType)));
}</code></pre>
<p>上述我们讲的都是针对全局的约定,我们在开发工程中大部分遇到的是针对符合特定条件的模型进行约定,此时我们就用到了自定义特性。我们先来看一段代码:</p>
<pre><code class="csharp">[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NoUnicode : Attribute
{
}</code></pre>
<p>这段代码将类型为字符串的属性配置为非Unicode,下面我们建上面的特性应用到所有模型</p>
<pre><code class="csharp">protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties().Where(x => x.GetCustomAttributes(false).OfType<NoUnicode>().Any())
.Configure(c => c.IsUnicode(false));
}</code></pre>
<p>添加该特性后,映射在数据库中的列将是 <strong><em>varchar</em></strong> 类型,而不是 nvarchar 类型。但是上述代码存在一个问题,如果匹配的不是字符串类型将会报错,因此我们将代码更新如下:</p>
<pre><code class="csharp">protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties().Where(c => c.GetCustomAttributes(false).OfType<NoUnicode>().Any())
.Configure(c => c.IsUnicode(false));
modelBuilder.Properties().Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, attr) => config.IsUnicode(attr.Uniconde));
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NoUnicode : Attribute
{
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Uniconde { get; set; }
public IsUnicode(bool isUnicode)
{
Uniconde = isUnicode;
}
}</code></pre>
Entity Framework 小知识(二)
https://segmentfault.com/a/1190000019465698
2019-06-13T11:12:48+08:00
2019-06-13T11:12:48+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、基于代码配置</h4>
<p>基于代码配置是EF6新增的一个特性,操作步骤如下:</p>
<ol>
<li>创建<strong><em>DbConfig</em></strong>派生类;</li>
<li>配置默认连接工厂;</li>
<li>设置<strong><em>Database Provider</em></strong>;</li>
<li>设置数据库初始化器;</li>
</ol>
<h5>1. 创建<strong><em>DbConfig</em></strong>派生类</h5>
<pre><code class="csharp">public class EF6Config:DbConfiguration
{
public EF6Config(){}
}
</code></pre>
<p>接下来使用 <strong><em>DbConfigurationType</em></strong> 属性在上下文类中设置基于代码的配置类:</p>
<pre><code class="csharp">[DbConfigurationType(typeof(EF6Config))]
public partial class EF6DbContext:DbContext
{
public EF6DbContext():base("name=EF6DbContext"){}
}</code></pre>
<h5>2. 配置默认连接工厂</h5>
<p>使用 <strong><em>SetDefaultConnectionFactory</em></strong> 方法设置默认连接工厂(以SQL SERVER 数据库为例):</p>
<pre><code class="csharp">public class EF6Config:DbConfiguration
{
public EF6Config()
{
this.SetDefaultConnectionFactory(new System.Data.Entity,Infrastructure.SqlConnectionFactory());
}
}</code></pre>
<h5>3. 设置<strong><em>Database Provider</em></strong>
</h5>
<p>使用 <strong><em>SetProviderServices()</em></strong> 方法配置数据库提供程序:</p>
<pre><code class="csharp">public class EF6Config:DbConfiguration
{
public EF6Config()
{
this.SetDefaultConnectionFactory(new System.Data.Entity,Infrastructure.SqlConnectionFactory());
this.SetProviderServices("System.Data.SqlClient",System.Data.Entity.SqlServer.SqlProviderServices.Instance);
}
}</code></pre>
<h5>4. 设置数据库初始化器</h5>
<p>在使用 code first 的情况下,可以使用基于代码的配置数据库的初始值:</p>
<pre><code class="csharp">public class EF6Config:DbConfiguration
{
public EF6Config()
{
this.SetDefaultConnectionFactory(new System.Data.Entity,Infrastructure.SqlConnectionFactory());
this.SetProviderServices("System.Data.SqlClient",System.Data.Entity.SqlServer.SqlProviderServices.Instance);
this.SetDatabaseInitializer<EF6DbContext>(new CustomDBInitializer(EF6DbContext)());
}
}</code></pre>
<blockquote>注:.config 中 <entityframework> 的配置优于代码配置,也就是说,如果同时在 .config 中和代码中都设置了配置选项,则优先使用 .config 中的设置。</blockquote>
Entity Framework 小知识(一)
https://segmentfault.com/a/1190000019212060
2019-05-17T13:40:43+08:00
2019-05-17T13:40:43+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、EF初始化数据库策略</h4>
<p>上一节我们演示的是在数据库不存在的情况下操作数据库的方法,但是某些情况下数据库是已经存在的(例如:对已有系统的升级改造扩展、DBA已经将数据库提前创建等等)。那么这种情况下我们该怎么办呢?这时我们就用到了EF数据库初始化的三种策略。这三种策略如下:</p>
<ol><li>如果数据库不存在,则创建,存在,则不创建</li></ol>
<pre><code class="CSharp">Database.SetInitializer(new CreateDatabaseIfNotExists<EfDbContext>());</code></pre>
<ol><li>不管数据库是否存在,都创建</li></ol>
<pre><code class="CSharp">Database.SetInitializer(new DropCreateDatabaseAlways<EfDbContext>());</code></pre>
<ol><li>如果数据库模型发生变化,更新数据库</li></ol>
<pre><code class="CSharp">Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfDbContext>());</code></pre>
<blockquote>注:以上三种策略需要在EF上下文派生类中的构造函数中定义。</blockquote>
<p>上述三种策略是定义在代码中的,我们也可以将他们定义在配置文件中,我们以第三种策略为例,在 <strong><em>.config</em></strong> 文件中的AppSettings节点下配置:</p>
<pre><code class="xml"><appSettings>
<add key="DatabaseInitializerForType _2_1Code.EfDbContext,_2_1Code" value="System.Data.Entity.DropCreateDatabaseIfModelChanges,EntityFramework"/>
</appSettings>
</code></pre>
<blockquote>注:DatabaseInitializerForType 后面是派生类的位置(命名空间.DbContext派生类),逗号后面是派生类所在的命名空间。</blockquote>
<h4>一、禁用初始化策咯</h4>
<p>某些情况下我们不需要使用EF的数据库初始化策略,这时我们可以在代码或配置文件中设置,如下:</p>
<pre><code class="CSharp">Database.SetInitializer<EfDbContext>(null);</code></pre>
<blockquote>注:以代码方式禁用初始化策略,也学要在EF上下文派生类中的构造函数中定义</blockquote>
<pre><code class="xml"><appSettings>
<add key="DatabaseInitializerForType _2_1Code.EfDbContext,_2_1Code" value="Disabled"/>
</appSettings></code></pre>
Entity Framework初体验
https://segmentfault.com/a/1190000019169701
2019-05-13T23:40:33+08:00
2019-05-13T23:40:33+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、初体验</h4>
<ol>
<li>新建控制台程序,名称为:<strong><em>MyFirstEF</em></strong>
</li>
<li>在NuGet中搜索 <strong><em>Entity Framework</em></strong>,如下图:</li>
</ol>
<p><img src="/img/remote/1460000019169704?w=1822&h=845" alt="图片" title="图片"></p>
<ol><li>创建 <strong><em>Blog</em></strong> 类:</li></ol>
<pre><code class="CSharp">public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public DateTime? CreatedTime { get; set; }
public double Double { get; set; }
public float Float { get; set; }
}</code></pre>
<ol><li>创建一个继承自EF上下文的类,此上下文是与数据库交互的一个中间桥梁,我们可以称之为会话,并且为每一个模型公开一个DbSet:</li></ol>
<pre><code class="CSharp">public class EfDbContext : DbContext
{
public EfDbContext()
{
}
public DbSet<Blog> Blogs { get; set; }
}</code></pre>
<blockquote>
<p>注:上下文派生类中定义DbSet有如下三种方式:</p>
<pre><code class="CSharp">//用DbSet属性
public class EfDbContext : DbContext
{
public EfDbContext()
{
}
public DbSet<Blog> Blogs { get; set; }
}
//用IDbSet属性
public class EfDbContext : DbContext
{
public IDbSet<Blog> Blogs { get; set; }
}
//只读属性
public class EfDbContext : DbContext
{
public DbSet<Blog> Blogs
{
get {return Set<Blog>();}
}
}</code></pre>
</blockquote>
<ol><li>在主函数上添加如下代码:</li></ol>
<pre><code class="CSharp">static void Main(string[] args)
{
using (var efDbContext = new EfDbContext())
{
efDbContext.Blogs.Add(new Blog()
{
Name = "张三",
Url = "http://www.baidu.com"
});
efDbContext.SaveChanges();
}
}</code></pre>
<ol><li>运行控制台程序,如果未出现任何报错,则会在VS对应的本地数据库中看到新创建的 <strong><em>Blogs</em></strong> 表和一条新数据。</li></ol>
<p><a href="https://link.segmentfault.com/?enc=fHqHf6%2FNnsnpqqL1AUICYA%3D%3D.UFAc7DGNzKdYRI3EFgGufqtM%2Fu8BHwzbz3tjT2Py16s%3D" rel="nofollow"><img src="/img/remote/1460000019169705" alt="E5JAMR.png" title="E5JAMR.png"></a></p>
<blockquote>注:如果未找到或无法访问服务器的错误,则说明你本地vs未安装LocalDB数据库,这时你可以安装LocalDB数据库,或者在App.config中将连接字符串修改为SQL Server 数据库的地址。</blockquote>
Entity Framework简介
https://segmentfault.com/a/1190000019148593
2019-05-11T18:00:23+08:00
2019-05-11T18:00:23+08:00
喵叔
https://segmentfault.com/u/miaoshu
0
<h4>零、什么是Entity Framework</h4>
<p>Entity Framework (简称EF),是.NET的 <strong>Object/Relational Mapping</strong> 实体框架(简称ORM),可以在 SQL Server、MySQL、Oracle、等数据库上使用。可以将数据作为业务对象和实体进行操作,使用LINQ进行查询,使用C#进行操作和检索。</p>
<h4>一、领域建模方式</h4>
<p>Entity Framework 有三种领域建模方式:<strong>Code First</strong>、<strong>Model First</strong>和<strong>Data First</strong></p>
<ol>
<li>
<p>Code First</p>
<p>Code First 可以通过类来描述模型,然后通过类来创建数据库,这种类简称为POCO(Plain Old CLR Object)。POCO中的C是指 .NET Framework公共语言运行时(Common Language Runtime,CLR)中的一个简单对象。POCO对域对象使用尽可能简单的类,可以包含属性、方法等,但是方法不能实现持久化逻辑,也就是说POCO也可以包含业务逻辑。Code First 优点如下:</p>
<ul>
<li>可以创建一个更富有逻辑、更灵活的应用程序;</li>
<li>因为没有自动生成难以修改的代码,所以我们可以对代码完全控制;</li>
<li>只需要定义映射,其余一切交给Entity Framework来处理;</li>
<li>可以用修改代码的方式来修改数据库;</li>
<li>可以使用它来映射表结构到一个已存在的数据库。</li>
</ul>
</li>
<li>
<p>Model First</p>
<p>Model First 允许我们使用实体设计器在空模型中创建模型实体,及其关系和继承层次结构,然后创建数据库。优缺点如下:</p>
<ul>
<li>无法控制实体和数据库,因为自动生成的代码难以修改,但是对于小型且简单的项目,它仍行之有效;</li>
<li>在实体中添加额外的功能,不得不修改T4模板或者使用部分类来完成;</li>
<li>数据库模型的更改不是最佳选择,因为是由模型定义了数据库。</li>
</ul>
</li>
<li>
<p>Data First</p>
<p>Data First 使我们能够从现有数据库创建模型,减少了自动生成代码所需编写的代码量,也限制了我们使用生成代码的结构。优缺点如下:</p>
<ul>
<li>如果已有DBA设计的数据来单独开发或已存在数据库,将作为首选</li>
<li>通过EDM向导为我们创建实体、关系和继承层次结构,修改映射后还可以生成实体;</li>
<li>要在实体中添加额外的功能,必须通过T4修改模板或者使用部分类;</li>
<li>数据库的手动更改变为可能,如果要修改数据库表结构,只需要从数据库更新实体模型即可。</li>
</ul>
</li>
</ol>