doctrine2到底是个什么玩意

之前和最近一个项目用到了Doctrine,由于是别人搭建的,自己没有很了解,最近又开始做的时候发现拙荆见肘,于是看了一下doctrine教程,本文就是加上自己理解的doctrine教程文档笔记了。

Doctrine2 配置需求

需要php5.3.3及以上

可以使用composer安装

什么是Doctrine?

Doctrine是一个ORM(Object-relational mapper),提供php数据库和PHP对象的映射。他和其他的ORM一样都是为了保证持久层和逻辑层的分类而存在的。

什么是Entity

Entity是PHP的一个对象

Entity对应的表需要有主键

Entity中不能含有final属性或者final方法

教程:

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/getting-started.html

教程代码的github地址在:

https://github.com/doctrine/doctrine2-orm-tutorial

使用composer安装

doctrine是可以根据Entity代码来生成数据表的

在src文件夹(config = Setup::createAnnotationMetadataConfiguration(array(__DIR__."/src"),isDevMode);)

中写出Entity代码

然后使用


1

php vendor/bin/doctrine orm:schema-tool:create

工具来生成数据表

用下面的命令更新数据表


1

$ php vendor/bin/doctrine orm:schema-tool:update --force --dump-sql

当然在bootstrap中需要设置连接mysql数据库


1

2

3

4

5

6

7

8

9

10

11

12

// database configuration parameters

$conn = array(

    'driver' => 'pdo_mysql',

    'host' => 'localhost',

    'user' => 'yjf',

    'password' => 'yjf',

    'dbname' => 'yjf',

    'path' => __DIR__ . '/db.sql',

);

 

// obtaining the entity manager

$entityManager = EntityManager::create($conn, $config);

其中Entity除了可以使用代码来进行设置外,也可以使用xml和yml文件进行设置。

它的命令行读取的是cli-config.php这个配置数据,所以在使用doctrine的命令行之前,需要先编写这个配置文件。

使用EntityManager能对Entity进行增删改查的操作


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

增加:

$product = new Product();

$product->setName($newProductName);

 

$entityManager->persist($product);

$entityManager->flush();

 

查询:

$entityManager->find('Product', $id)

 

更新:

$product = $entityManager->find('Product', $id);

$product->setName($newName);

$entityManager->flush();

 

删除:

$product = $entityManager->find('Product', $id);

$product->remove();

$entityManager->flush();

如何调试

doctrine由于EntityManager结构复杂,所以使用var_dump()返回的数据及其庞大,并且可读性差。应该使用

Doctrine\Common\Util\Debug::dump()来打印信息。

表之间的关联如何体现在Entity上

首先明确表和表的关联有几种:

一对一

一对多

多对一

多对多

比如教程中举的例子,bug系统

bug表和user表分别存储bug信息和user信息

每个bug有个工程师engineer的属性,代表这个bug是由哪个工程师开发的,那么就有可能有多个bug是由一个工程师开发的。所以bug表对于user表就是多对一的关系。user表对于bug表就是一对多的关系。

bug表和product表分别存储bug信息和出bug的产品信息

一个bug可能有多个产品一起出现问题导致的,而一个产品也有可能有多个bug,所以bug表和product表就是多对多的关系。

一对一的关系就比较简单了。

一对多如何设置

这里主要说下一对多和多对一的时候,在bug的Entity中和user的Entity中应该对应这样设置:


1

2

3

4

5

6

7

8

9

10

11

12

#bug.php

    /**

     * @ManyToOne(targetEntity="User", inversedBy="assignedBugs")

     **/

    protected $engineer;

 

#user.php

    /**

     * @OneToMany(targetEntity="Bug", mappedBy="engineer")

     * @var Bug[]

     **/

    protected $assignedBugs = null;

这里ManyToOne和OneToMany是互相对应的,inversedBy和mappedBy也是互相对应的。

这样如果使用doctrine自动生成表结构:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

mysql> show create table users;

 

| users | CREATE TABLE `users` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |

 

 

mysql> show create table bugs;

 

| bugs  | CREATE TABLE `bugs` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `engineer_id` int(11) DEFAULT NULL,

  `reporter_id` int(11) DEFAULT NULL,

  `description` varchar(255) COLLATE utf8_unicode_ci NOT NULL,

  `created` datetime NOT NULL,

  `status` varchar(255) COLLATE utf8_unicode_ci NOT NULL,

  PRIMARY KEY (`id`),

  KEY `IDX_1E197C9F8D8CDF1` (`engineer_id`),

  KEY `IDX_1E197C9E1CFE6F5` (`reporter_id`),

  CONSTRAINT `FK_1E197C9E1CFE6F5` FOREIGN KEY (`reporter_id`) REFERENCES `users` (`id`),

  CONSTRAINT `FK_1E197C9F8D8CDF1` FOREIGN KEY (`engineer_id`) REFERENCES `users` (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |

可以看出的是bugs生成了engineer_id属性,然后自动生成外键的索引。

更多的entity和mysql的对应关系是:

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html

如何使用DQL进行查询

对,你没有看错,这里是DQL而不是SQL。DQL是毛语言呢?Document Query Language,意思就是文档化的sql语句,为什么sql语句需要文档化呢?sql语句更倾向于表结构实现,所以在写sql语句的时候头脑中需要具现化的是表结构,而ORM的目的就是不需要开发者关注表结构,所以需要一个不基于表结构的查询语句,又能直接翻译成为SQL语句,这就是DQL。DQL可以直接对Entity进行增删改查,而不需要直接对表进行操作。

比如下面的一个例子:

$dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ORDER BY b.created DESC";

$query = $entityManager->createQuery($dql);
$query->setMaxResults(30); 
$bugs=$query->getResult();

看这里的sql中From的就是“Bug”,这个是Entity的类,其实熟悉了sql,dql的查询语法也是一模一样的。

考虑使用dql而不是sql除了和ORM目标一致外,还有一个好处,就是存储层和逻辑层的耦合分开了。比如我的存储层要从mysql换成mongodb,那么逻辑层是什么都不需要动的。

使用Repository

到这里就嘀咕,我经常进行的查询是根据字段查询列表啥的,难道每次需要我都拼接dql么?当然不用,doctrine为我们准备了repository的概念,就是查询库,里面封装了各种查询常用的方法。

使用如下:

<?php
$product = $entityManager->getRepository('Product')
                         ->findOneBy(array('name' => $productName));

Repository对应的操作有:


1

2

3

4

5

find()

findAll()

findBy()

findOneBy()

findOneByXXX()

示例:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

<?php// $em instanceof EntityManager

$user = $em->getRepository('MyProject\Domain\User')->find($id);

 

<?php// $em instanceof EntityManager

 

// All users that are 20 years old

$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20));

 

// All users that are 20 years old and have a surname of 'Miller'

$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20, 'surname' => 'Miller'));

 

// A single user by its nickname

$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('nickname' => 'romanb'));

 

 

<?php// A single user by its nickname

$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('nickname' => 'romanb'));

 

// A single user by its nickname (__call magic)

$user = $em->getRepository('MyProject\Domain\User')->findOneByNickname('romanb');

 

doctrine还允许自己创建Repository,然后只需要在Entity中说明下repositoryClass就可以了。


1

2

3

4

5

6

7

class UserRepository extends EntityRepository

{

    public function getAllAdminUsers()

    {

        return $this->_em->createQuery('SELECT u FROM MyDomain\Model\User u WHERE u.status = "admin"')

                         ->getResult();

    }}

如何使用原生的sql语句来做查询?

除了dql之外,doctrine也允许使用原生的sql语句来做查询。这篇教程有最详细的说明http://docs.doctrine-project.org/en/latest/reference/native-sql.html

主要是提供了createNativeQuery的方法


1

2

3

4

5

6

7

8

9

10

11

12

13

<?php

// Equivalent DQL query: "select u from User u where u.name=?1"

// User owns an association to an Address but the Address is not loaded in the query.

$rsm = new ResultSetMapping;

$rsm->addEntityResult('User', 'u');

$rsm->addFieldResult('u', 'id', 'id');

$rsm->addFieldResult('u', 'name', 'name');

$rsm->addMetaResult('u', 'address_id', 'address_id');

 

$query = $this->_em->createNativeQuery('SELECT id, name, address_id FROM users WHERE name = ?', $rsm);

$query->setParameter(1, 'romanb');

 

$users = $query->getResult();

 

这里唯一让人不解的就是ResultSetMapping了,ResultSetMapping也是理解原生sql查询的关键。

其实也没什么不解的了,ORM是不允许数据库操作返回的不是Object的,所以ResultSetMapping就是数据库数据和Object的结构映射。

这个Mapping也可以在Entity中进行设置。

如何使用QueryBuilder

QueryBuilder是doctrine提供的一种在DQL之上的一层查询操作,它封装了一些api,提供给用户进行组装DQL的。

QueryBuilder的好处就是看起来不用自己字符串拼装查询语句了。

关于QueryBuilder的详细说明可以看这篇文章:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/query-builder.html


1

2

3

4

5

6

7

8

<?php

// $qb instanceof QueryBuilder

$qb = $em->createQueryBuilder();

 

$qb->select('u')

   ->from('User', 'u')

   ->where('u.id = ?1')

   ->orderBy('u.name', 'ASC');

 

QueryBuilder的接口有:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

<?phpclass QueryBuilder{

    // Example - $qb->select('u')

    // Example - $qb->select(array('u', 'p'))

    // Example - $qb->select($qb->expr()->select('u', 'p'))

    public function select($select = null);

 

    // Example - $qb->delete('User', 'u')

    public function delete($delete = null, $alias = null);

 

    // Example - $qb->update('Group', 'g')

    public function update($update = null, $alias = null);

 

    // Example - $qb->set('u.firstName', $qb->expr()->literal('Arnold'))

    // Example - $qb->set('u.numChilds', 'u.numChilds + ?1')

    // Example - $qb->set('u.numChilds', $qb->expr()->sum('u.numChilds', '?1'))

    public function set($key, $value);

 

    // Example - $qb->from('Phonenumber', 'p')

    public function from($from, $alias = null);

 

    // Example - $qb->innerJoin('u.Group', 'g', Expr\Join::WITH, $qb->expr()->eq('u.status_id', '?1'))

    // Example - $qb->innerJoin('u.Group', 'g', 'WITH', 'u.status = ?1')

    public function innerJoin($join, $alias = null, $conditionType = null, $condition = null);

 

    // Example - $qb->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, $qb->expr()->eq('p.area_code', 55))

    // Example - $qb->leftJoin('u.Phonenumbers', 'p', 'WITH', 'p.area_code = 55')

    public function leftJoin($join, $alias = null, $conditionType = null, $condition = null);

 

    // NOTE: ->where() overrides all previously set conditions

    //

    // Example - $qb->where('u.firstName = ?1', $qb->expr()->eq('u.surname', '?2'))

    // Example - $qb->where($qb->expr()->andX($qb->expr()->eq('u.firstName', '?1'), $qb->expr()->eq('u.surname', '?2')))

    // Example - $qb->where('u.firstName = ?1 AND u.surname = ?2')

    public function where($where);

 

    // Example - $qb->andWhere($qb->expr()->orX($qb->expr()->lte('u.age', 40), 'u.numChild = 0'))

    public function andWhere($where);

 

    // Example - $qb->orWhere($qb->expr()->between('u.id', 1, 10));

    public function orWhere($where);

 

    // NOTE: -> groupBy() overrides all previously set grouping conditions

    //

    // Example - $qb->groupBy('u.id')

    public function groupBy($groupBy);

 

    // Example - $qb->addGroupBy('g.name')

    public function addGroupBy($groupBy);

 

    // NOTE: -> having() overrides all previously set having conditions

    //

    // Example - $qb->having('u.salary >= ?1')

    // Example - $qb->having($qb->expr()->gte('u.salary', '?1'))

    public function having($having);

 

    // Example - $qb->andHaving($qb->expr()->gt($qb->expr()->count('u.numChild'), 0))

    public function andHaving($having);

 

    // Example - $qb->orHaving($qb->expr()->lte('g.managerLevel', '100'))

    public function orHaving($having);

 

    // NOTE: -> orderBy() overrides all previously set ordering conditions

    //

    // Example - $qb->orderBy('u.surname', 'DESC')

    public function orderBy($sort, $order = null);

 

    // Example - $qb->addOrderBy('u.firstName')

    public function addOrderBy($sort, $order = null); // Default $order = 'ASC'}

更多更复杂的查询可以查看上文的链接。

查询结果是只能返回对象吗?

当然不只,当你执行query的时候可以试试使用:


1

2

3

4

5

$result = $query->getResult();

$single = $query->getSingleResult();

$array = $query->getArrayResult();

$scalar = $query->getScalarResult();

$singleScalar = $query->getSingleScalarResult();

doctrine查询操作总结

现在总结下,doctrine2 做查询操作有下面几种方法

1 使用EntityManager直接进行find查询

2 使用DQL进行createQuery($dql)进行查询

3 使用QueryBuilder进行拼装dql查询

4 使用Repository进行查询

5 使用原生的sql进行createNativeQuery($sql)进行查询

doctrine2的增加,删除,更新操作都需要使用Entity进行操作

一个项目有几个实现路径:

1 Code First:先用代码写好Object,然后根据Object生成数据库

2 Model First:先用工具写好UML,然后根据UML生成数据库和PHP代码

3 Database First:先写好数据库的schema表,然后生成PHP代码

如何做分页操作

分页操作是经常使用到的,doctrine使用了Paginator类来做这个操作

比如:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

<?php

// list_bugs_array.php

use Doctrine\ORM\Tools\Pagination\Paginator;

require_once "bootstrap.php";

 

$dql = "SELECT b, e, r, p FROM Bug b JOIN b.engineer e ".

       "JOIN b.reporter r JOIN b.products p ORDER BY b.created DESC";

$query = $entityManager->createQuery($dql)

               ->setFirstResult(0)

               ->setMaxResults(1);

 

$paginator = new Paginator($query, $fetchJoinCollection = true);

 

$c = count($paginator);

echo "count: $c" . "\r\n";

 

$bugs = $query->getArrayResult();

 

foreach ($bugs as $item) {

     print_r($item);

}

exit;

返回了总条数2,也返回了查询的结果。赞~!

如何进行sql和dql的调试

我们不免调试的时候要取出sql和dql语句。我们可以使用


1

2

$query->getDQL()

$query->getSQL()

来获取出实际进行查询的sql语句

为什么在增删更新的时候有个flush操作

doctrine在增加,删除,更新的时候并不是直接进行操作,而是将操作存放在每个EntityManager的UnitOfWork。

你可以使用


1

2

3

4

$entityManager->getUnitOfWork()

$entityManager->getUnitOfWork()->size()

$entityManager->getEntityState($entity)

来控制UnitOfWork


1

  

如何注入Entity增加,删除,更新操作

doctrine提供了监听Event的功能,比如你要在Persist之前做一个日志处理,你就可以实现一个Listener,其中实现了prePersist方法 
然后把Listener挂载到Entity上


1

2

3

4

5

6

7

8

<?php

namespace MyProject\Entity;

 

/** @Entity @EntityListeners({"UserListener"}) */

class User

{

    // ....

}

如何实现事务?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

<?php

// $em instanceof EntityManager

$em->getConnection()->beginTransaction(); // suspend auto-commit

try {

    //... do some work

    $user = new User;

    $user->setName('George');

    $em->persist($user);

    $em->flush();

    $em->getConnection()->commit();

} catch (Exception $e) {

    $em->getConnection()->rollback();

    $em->close();

    throw $e;

}

使用DQL只能进行查询操作吗?

当然不只,我们可以使用execute()来对增删改查的DQL语句进行操作


1

2

3

<?php

$q = $em->createQuery('delete from MyProject\Model\Manager m where m.salary > 100000');

$numDeleted = $q->execute();

Entity可以设置哪些属性:

参考文章:

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html

有哪些Cache机制

doctrine可以支持APC,Memcache,Xcache,Redis这几种缓存机制

所有这些缓存机制都是基于一个抽象方法,这个抽象方法中有的接口有:


1

2

3

4

fetch($id)

contains($id)

save($id, $data, $lifeTime=false)

delete($id)

各自对应的初始化代码:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

APC:

<?php

$cacheDriver = new \Doctrine\Common\Cache\ApcCache();

$cacheDriver->save('cache_id', 'my_data');

 

MemCache:

<?php

$memcache = new Memcache();

$memcache->connect('memcache_host', 11211);

 

$cacheDriver = new \Doctrine\Common\Cache\MemcacheCache();

$cacheDriver->setMemcache($memcache);

$cacheDriver->save('cache_id', 'my_data');

 

Xcache:

<?php

$cacheDriver = new \Doctrine\Common\Cache\XcacheCache();

$cacheDriver->save('cache_id', 'my_data');

 

Redis:

<?php

$redis = new Redis();

$redis->connect('redis_host', 6379);

 

$cacheDriver = new \Doctrine\Common\Cache\RedisCache();

$cacheDriver->setRedis($redis);

$cacheDriver->save('cache_id', 'my_data');

还有一个命令clear-cache可以用来进行缓存的增删改查

具体参考文章:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/caching.html

doctrine提供的工具有哪些

参考文章:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/tools.html

也可以使用list来进行查看命令

看看这些命令,大致可以完成的功能是:

数据库schema生成php的Entity代码

php的Entity代码生成数据库schema

缓存相关操作

数据库schema相关操作

but对于这些命令:

但是自己试过才知道,有些还是有一些限制的的,比如它必须要求表必须有一个自增主键,而且并且是库中的所有表都有这个要求。。。才能生成ORM的Entity等数据。

还有生成entity也必须要先创建一个基本的模板之类的。

但是这些工具总的来说还是很有用的,聊胜于无。

总结

doctrine就是一个很庞大的ORM系统,它可以嵌入到其他框架中,比如symfony,比如Yii等。

ORM的最终目的就是将逻辑层和持久层分离,在这个层面来说,doctrine很好地完成了这个任务。

doctrine已经将你能考虑到的操作都进行封装好了,相信如果熟悉了之后,开发过程应该是会非常快的

时间: 2024-10-27 04:45:03

doctrine2到底是个什么玩意的相关文章

网络广告到底是个啥玩意?(3)

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断淘宝客 站长团购 云主机 技术大厅 中国第一个商业性网络广告是:1997年3月,IBM与英特尔联合在ChinaByte.com投下的. 屈指数来,网络广告在中国已经整整走过了十个年头.十年间,中国网民数量从62万猛增到了现在的1.44亿,中国网络广告市场规模也从零发展到了2006年底统计的65亿元. 记得当时在SINA与郑乃轩(绰号大熊,时任SINA创意总监,原供职Ch

JAVA查询Oracle数据库集群连接字符串

  事件: 报表接口数据库突然无法连接 ,导致无法正常取数操作. 异常信息: Io 异常: Got minus one from a read call 分析: 数据库地址及其配置信息都为发生变化 , 经询问后得知数据库调整为集群工作方式 . 结果: 1. 修改普通 JDBC 连接字符串为集群工作方式. 2. 接口中的 JDBC JAR文件不适合集群工作方式. 思维宽度: 1. JDBC JAR文件的选择, Classes12 到底是个啥玩意? 第一次听很晕 , 其实就是 oracle 数据库自

重拾热情之:网页重构都在做什么

这个标题目的很简单,就是为了让我们这些日复一日埋头码页面的网页重构工作者,重拾面对这个我们所热爱的行业的热情;检视经常被我们在繁琐的工作中忽略的初级错误;以新人的眼光和态度迸发热情认真对待每一个页面.基本上都是我最想表达和同样需要重新审视的东西,也希望可以作为给刚入行的新人们的一些小小指引. 一般来说,接到一个新的需求,以简单的少页面需求为例,流程上可以简单分为几大阶段: 1. 查看设计稿,阅读需求文档.原型图; 2. 查看并分析设计稿,在脑中整理出粗略解决方案; 3. 切图.合并图片; 4.

数据采集高并发的架构应用

问题描述 问题的出发点:最近公司为了发展需要,要扩大对用户的信息采集,每个用户的采集量估计约2W.如果用户量增加的话,将会大量照成采集量成3W倍的增长,但是又要满足日常业务需要,特别是指令要及时得到响应的频率次数远大于预期.注:公司架构采集.NET平台架构.技术障碍:1.面对用户量的增长,记录数2W倍的增长,如何保证这些记录能够在比较快的时间内进入存储介质. 2.应对用户量的增长,如何在规定的时间内完成采集,增加硬件设备处理能力还是使用更多的服务器来处理请求.3.服务器的增长,是否能够支持现有的

C++ 学习之旅二——说一说C++头文件

  作为一个二手的.net程序员,你看到了C++头文件一定就犯迷糊了,这到底是个啥玩意.再我纠结了24个小时, google20次,度娘10下,看过10来骗文章以后,我可能稍微开窍了.我对C++头文件总结,与.net比较如下:    一.C++头文件究竟是什么,你怎么看? 每个C++/C程序通常分为两个文件.一个文件用于保存程序的声明(declaration),称为头文件.另一个文件用于保存程序的实现(implementation),称为定义(definition)文件.C++/C程序的头文件以

与手机网速、通话质量千丝万缕的通信基带

自从我们进入了4G时代,各家手机厂商处理器新品发布会现场都会听到支持全网通.x模x频等等一系列与通信网络有关的专有名词,这些技术最主要来自手机中的一个名为基带的芯片支持.那么什么是基带芯片呢? 什么是基带? 对于基带一词,想必很多网友都是耳熟能祥的东西,毕竟在"远古"时代的Android是可以随意刷写基带,就像更新ROM一样简单,刷入新基带过后还可能对手机的信号.通话质量有一定增强.不过呢在这里,我们所指的基带是硬件上的基带芯片. 基带芯片就是手机中的通信模块,最主要的功能就是负责与移

我的女神——简洁实用的iOS代码调试框架

我的女神--简洁实用的iOS代码调试框架 一.引言         这篇博客的起源是接手了公司的一个已经完成的项目,来做代码优化,项目工程很大,并且引入了很多公司内部的SDK,要搞清楚公司内部的这套框架,的确不是件容易的事,并且由于这个项目是多人开发的,在调试阶段会打印出巨量的调试信息,使得浏览有用信息变的十分困难,更加恐怖的是,很多信息是SDK中的调试打印,将这些都进行注销是非常费劲甚至不可能的事,于是便有了这样一些需求:首先,我需要清楚了解各个controller之间的跳转关系,需要快速的弄

什么,YouTube, App这些英文你都读错了

但是,你又知不知道其实外国人听不懂你们讲的YouTube,APP到底是个啥玩意. 因为,你读的跟外国人说的不一样. 来看看我们常念错的社交媒体英文都有哪些: 1.Skype 很多人会不自觉念成"Sky Pe 死该皮",但其实这本身就是一个单词,不需要拆分开来读. 2. YouTube 大家可千万不要念成"You Tu Be 优兔逼" 3.Instagram 不是你说的"Ins Ta Grum 因斯特馆" 4.Line 由于发音习惯,有些人也许会直

SEO公司如何留住宝贵的人才资源

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断 淘宝客 云主机 技术大厅 大家好,我是梁磊.随着SEO的火爆,越来越多的SEO公司和工作室如雨后春笋般出现了,面对一个刚起步不久的小型SEO公司或者工作室,我想做难的就是招聘到合适的SEO人才,所以很多SEO公司或者工作室都比较倾向于应届毕业生,但是这样的公司或者工作室很难留住人才,辛苦培养的人才大量流失了,这对公司是一个损失.而最普遍的现象是许多童鞋做不到几个月就离