首页 » 网站建设 » phporm库技巧_ORM哪家强javacphppython go一一比拟 网友直呼周全客不雅观

phporm库技巧_ORM哪家强javacphppython go一一比拟 网友直呼周全客不雅观

访客 2024-11-07 0

扫一扫用手机浏览

文章目录 [+]

为了让这个库更好用,我比较研究了各措辞的主流ORM库,创造有一些措辞的ORM库确实很好用,而有其余一些措辞的库那不是一样平常的难用。

然后我总结了他们呢的一些共性和差异点,于是形成了本文的紧张内容。

phporm库技巧_ORM哪家强javacphppython go一一比拟 网友直呼周全客不雅观

本文会先解释什么是SQL编写难题,以及磋商一下 code first 和 database first 的优缺陷。
然后依据这两个问题的结论去核阅目前主流后端措辞java, c#, php, python, go各自的orm库,比拟研究下他们的优缺陷。
末了给出总结和参考文档。

phporm库技巧_ORM哪家强javacphppython go一一比拟 网友直呼周全客不雅观
(图片来自网络侵删)

如果你须要做技能选型,或者做技能研究,或者类似于我做框架开拓,或者纯挚地理解各措辞的差异,或者便是想吹个牛,建议保存或收藏。
如果本文所涉及到的内容有任何禁绝确,欢迎批评示正。

温馨提示,本文会有一些戏谑或者调侃身分,并非对某些措辞或者措辞的利用者有任何歧视见地。
如果对你造成了某些侵害,请多包涵。

什么是SQL编写难题

如果你是做web开拓,那么一定须要保存数据到数据库,这个时候你必须熟习利用sql语句来读写数据库。

sql本身不难,命令也就那几个,关键字也不算多,但是为什么编写sql会成难堪题呢?

比如下面的sql

select from user insert user (name,mobile) values ('tang','18600000000')

它有什么难题? 大略的单表操作嘛,一点难题没有,但凡学过点sql的程序员都能写出来,并且担保精确。
我估计比例能超过90%

但是,如果你须要写下面的sql呢?

SELECT article., person.name as person_name FROM article LEFT JOIN person ON person.id=article.person_id WHERE article.type = 0 AND article.age IN (18,20)

这个也不繁芜,便是你在做查询列表的时候,会常常用到的联表查询。
你是否还有勇气说,写出来的sql绝瞄准确。
我估计比例不超过70%

再轻微繁芜点,如果是下面的sql?

SELECT o., d.department_name, (SELECT Sum(so.goods_fee) AS task_detail_target_completed_tem FROM sale_order so WHERE so.merchant_id = '356469725829664768' AND so.create_date BETWEEN (20230127) AND (20230212) AND so.delete_state = 2 AND so.department_id = o.department_id ) AS task_detail_target_completed FROM task_detail o LEFT JOIN department d ON d.department_id=o.department_id WHERE o.merchant_id = '356469725829664768' AND o.task_id = '356469725972271104768'

这是我项目里真实的sql语句,目的是统计出所有部门在某韶光段内各自的古迹。
逻辑上也不太繁芜,但你是否还有勇气说,写出来的sql绝瞄准确。
我估计比例不超过40%

如上面的sql所示,SQL编写难题在于以下几方面。

要担保字段精确

该当有的字段不能少,不应该有的字段不能多。

比如你把mobile误打成mobike,这属于拼写缺点,但是这个拼写缺点只有在实际运行的时候才会见告你字段名错了。

并且项目越大,表越多,字段越多,这种拼写缺点发生的可能性越大。
以至于可以肯定的说,100%的可能性会涌现。

要特殊把稳sql语法

例如你在查询的时候必须写from,绝对不能误写成form,但是在实际开拓过程中,很随意马虎就打错了。

这种缺点,也只有运行的时候才会见告你语法错了。
并且sql越繁芜,这种语法缺点发生的可能性越大。

编辑器不会有sql的语法提示

常见的编码用的软件,对付sql干系的代码,不会有语法提示,也不会有表名提示,字段名提示。

终极的代码质量如何全凭你的目光,履历,能力。

很显然,既然存在该难题,那么哪个ORM能办理该难题,就该当算得上好,如果不能办理,则不能称之为好。

什么是code first 和 database first

这俩观点并不是新观点,但是我估计大多数开拓者并不熟习。

所谓 code first, 附近的词是 model fist, 意思是模型优先,指的是在设计和开拓系统时,优先和重点做的事情是设计业务模型,然后根据业务模型去创建数据库。

所谓 database first,意思是数据库优先,指的是在设计和开拓系统时,优先和重点做的事情是创建数据库构造,然后去实现业务。

这里我提到了几个词语,可能在不同的措辞里叫法不一样,可能不同的人的叫法也不一样,为了下述方便,我们举例子来说。

code first 例子

假设我是一个对电商系统完备不懂的小白,手头上也没有如何设计电商系统的资料,我和我的伙伴只是模糊地知道电商系统紧张业务便是处理订单。

然后我大概会知道这个订单,紧张的信息包括哪个用户下单,什么韶光下单,有哪几种商品,数量分别是多少,根据这些已有的信息,我可以设计出来业务模型如下

public class OrderModel { //订单编号 Integer orderId; //用户编号 Integer userId; //订单韶光 Integer createTime; //订单详情(包含商品编号,商品数量) String orderDetail;}

很大略,对吧,这个模型很匹配我目前对系统的认知。
接下来会做各种业务逻辑,末了要做的是将订单模型的数据保存到数据库。
但是在保存数据到数据库的时候,就有一些考虑了。

我可以将上面OrderModel业务模型建立一张对应表,里面的4个属性,对应数据表里的4个字段,这完备可以。
但是我是电商小白,不是数据库小白啊,这样存储的话,肯定不利于统计订单商品的。

以是我换一种策略,将OrderModel的信息进行拆分,将前三个属性 orderId, userId, createTime 放到一个新的类里。
然后将 orderDetail 的信息进行再次分解,放到另一个类里

public class OrderEntity { Integer orderId; Integer userId; Integer createTime;}public class OrderDetailEntity { Integer orderDetailId; Integer orderId; Integer goodsId; Integer goodsCount;}

末了,在数据库建立两张表order,order_detail,表构造分别对应类OrderEntity,OrderDetailEntity的构造。

至此,我们完成了从业务模型OrderModel到数据表order,order_detail的过程。

这便是 code first ,把稳这个过程的关键点,我优先考虑的是模型和业务实现,后面将业务模型数据进行分解和保存是次要的,非优先的。

database first 例子

假设我是一个对电商系统非常熟习的老鸟,之前做过很多电商系统,那么我在做新的电商系统的时候,就完备可以先设计数据库。

order表放订单紧张数据,里面有xxx几个字段,分别有什么浸染,有哪些状态值

order_detail表放订单详情数据,,里面有xxx几个字段,分别有什么浸染

这些都可以很清楚和明确。
然后根据表信息,天生OrderEntity,以及OrderDetailEntity即可开始接下来的编码事情。
这种情形下OrderModel可能有,也可能没有。

这便是 database first ,把稳这个过程的关键点,我优先考虑的是数据库构造和数据表构造。

两种办法比拟

code first 模式下, 系统设计者优先考虑的是业务模型OrderModel, 它可以描述清楚一个完全业务,包括它的所有业务细节(什么人的订单,什么时候的订单,订单包含哪些商品,数量多少),有利于设计者对付系统的整体把控。

database first 模式下, 系统设计者优先考虑的是数据表order,order_detail,他们中任何一张表都不能完全的描述清楚一个完全业务,只能够描述局部细节,不利于设计者对付系统的整体把控。

在这里,调皮的同学会问,在 database first 模式下, 我把order,order_detail的信息一起看,不就知道完全的业务细节了吗?

确实是这样,但这里有一个条件,条件是你必须明确的知道order,order_detail是须要一起看的,而你知道他们须要一起看的条件是你理解电商系统。
如果你设计的不是电商系统,而是电路系统,你还理解吗?还知道哪些表须要一起看吗?

至此,我们可以有以下粗浅的判断:

对付新项目,不熟习的业务,code first 模式更适宜一些

对付老项目,熟习的业务,database first 模式更得当一些

如果两种模式都可以的话,优先利用 code first 模式,便于理解业务,把控项目

如果哪个ORM支持 code first , 我们可以稍稍认为它更好一些

Java体系的orm

Java措辞是web开拓领域处于领先地位,这一点无可置疑。
它的优点很明显,但是缺陷也不是没有。

海内运用比较广泛的orm是Mybatis,以及衍生品Mybatis-plus等

实际上Mybatis团队还出了其余一款产品,MyBatis Dynamic SQL,海内我见用的不多,谈论都较少。
英文还可以的同学,可以看下面的文档。

其余还有 jOOQ, 实际上跟 MyBatis Dynamic SQL 非常类似,有兴趣的可以去翻翻

下面,我们举一些例子,来比拟一下他们的基本操作

Java体系的Mybatis

单就orm这一块,海内用的最多的该当是Mybatis,说到它的利用体验吧,那切实其实是一言难尽。

你须要先定义模型,然后编写xml文件用来映射数据,然后创建mapper文件,用来实行xml里定于的sql。
从这个流程可以看出,中间的xml文件起到核心浸染,里面不只有数据类型转换,还有最核心的sql语句。

范例的xml文件内容如下

<mapper namespace="xxx.mapper.UserMapper"> <insert id="insertUser" parameterType="UserEntity"> insert into user (id,name,mobile) values (#{id},#{name},#{mobile}) </insert> <update id="updateUser" parameterType="UserEntity"> update user set name = #{name}, mobile = #{mobile} where id = #{id} </update> <delete id="deleteUser"> delete from user where id = #{id} </delete> <select id="selectUsers" resultType="UserVO"> select u., (select count() from article a where a.uid=u.id) as article_count from user u where u.id = #{id} </select></mapper>

你在编写这个xml文件的时候,这个手写sql没有实质差异,一定会碰着刚才说到的SQL编写难题。

Java体系的Mybatis-plus

这里有必要提一下 Mybatis-plus,它是海内的团队开拓出来的工具,算是对Mybatis的扩展吧,它减少了xml文件内容的编写,减少了一些开拓的痛楚。
比如,你可以利用如下的代码来完成以上相同的事情

userService.insert(user); userService.update(user); userService.deleteById(user); List<UserEntity> userList = userService.selectList(queryWrapper);

完成这些事情,你不须要编写任何xml文件,也不须要编写sql语句,如之前所述,减少了一些开拓的痛楚。

但是,请你把稳我的用词,是减少了一些。

对付连表操作,嵌套查询等涉及到多表操作的事情,它就弗成了,为啥弗成,由于根本就不支持啊。
碰着这种情形,你就老诚笃实的去写xml吧,然后你还会碰着刚才说到的SQL编写难题。

Java体系的Mybatis3 Dynamic Sql

值得一提的是Mybatis3 Dynamic Sql,翻译一下便是动态sql。
还是刚才说的海内我见用的不多,谈论都较少,但是评价看上去挺好。

大略来说,可以根据不同条件拼接出sql语句。
不同于上面的Mybatis,这些sql语句是程序运行时天生的,而不是提前写好的,或者定义好的。

它的利用流程是,先在数据库里定义好数据表,然后创建模型文件,让然后通过命令行工具,将每一个表天生如下的支持文件

public final class PersonDynamicSqlSupport { public static final Person person = new Person(); public static final SqlColumn<Integer> id = person.id; public static final SqlColumn<String> firstName = person.firstName; public static final SqlColumn<LastName> lastName = person.lastName; public static final SqlColumn<Date> birthDate = person.birthDate; public static final SqlColumn<Boolean> employed = person.employed; public static final SqlColumn<String> occupation = person.occupation; public static final SqlColumn<Integer> addressId = person.addressId; public static final class Person extends SqlTable { public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER); public final SqlColumn<String> firstName = column("first_name", JDBCType.VARCHAR); public final SqlColumn<LastName> lastName = column("last_name", JDBCType.VARCHAR, "examples.simple.LastNameTypeHandler"); public final SqlColumn<Date> birthDate = column("birth_date", JDBCType.DATE); public final SqlColumn<Boolean> employed = column("employed", JDBCType.VARCHAR, "examples.simple.YesNoTypeHandler"); public final SqlColumn<String> occupation = column("occupation", JDBCType.VARCHAR); public final SqlColumn<Integer> addressId = column("address_id", JDBCType.INTEGER); public Person() { super("Person"); } }}

可以看出,这里的紧张功能能是将表内的字段,与java项目里的类里面的属性,做了逐一映射。

接下来你在开拓的时候,就不用关心表名,以及字段名了,直策应用刚才天生的类,以及类下面的那些属性。
详细如下

SelectStatementProvider selectStatement = select(id.as("A_ID"), firstName, lastName, birthDate, employed,occupation, addressId) .from(person) .where(id, isEqualTo(1)) .or(occupation, isNull()) .build() .render(RenderingStrategies.MYBATIS3); List<PersonRecord> rows = mapper.selectMany(selectStatement);

如上面的代码,好处有以下四点

你不再须要手写sql也不用在意字段名了,由于利用的都是类,或者属性,编写代码的时候编辑器会有提示,编译的时候如果有缺点也会提示,实际运行的时候就不会有问题了。
联表查询,嵌套查询啥的,也都支持完美避开了SQL编写难题

当然带来了额外的事情,比如你要利用工具来天生PersonDynamicSqlSupport类,比如你要先建表。

先建表这事儿,很明显就属于 database first 模式。

C#体系的orm

C# 在工业领域,游戏领域用的多一些,在web领域少一些。

它也有自己的orm,名字叫 Entity Framework Core, 一贯都是微软公司在掩护。

下面是一个范例的联表查询

var id = 1; var query = database.Posts .Join(database.Post_Metas, post => post.ID, meta => meta.Post_ID, (post, meta) => new { Post = post, Meta = meta } ) .Where(postAndMeta => postAndMeta.Post.ID == id);

这句代码的紧张浸染是,将数据库里的Posts表,与Post_Metas表做内联操作,然后取出Post.ID即是1的数据

这里涌现的Post,以及Meta都是提前定义好的模型,也便是类。
Post.ID 是 Post 的一个属性,也是提前定义好的。

全体功能的优点很多,你不再须要手写sql,不须要关心字段名,不须要天生额外类,也不会有语法缺点,你只须要提前定义好模型,完备没有SQL编写难题,很明显就属于 code first 模式。

比拟java的Mybatis以及Mybatis3 Dynamic Sql来说,你可以脑补一下下面的场景

PHP体系的orm

php体系内,框架也非常多,比如常见的laravel,symfony,这里我们就看这两个,比较有代表性

PHP体系的laravel

利用php措辞开拓web运用的大概多,个中比较出名的是laravel框架,比较范例的操作数据库的代码如下

$user = DB::table('users')->where('name', 'John')->first();

这里没有利用模型(就算利用了也差不多),代码里涌现的 users 便是数据库表的名字, name 是 users 表里的字段名,他们是被直接写入代码的

很明显它会产生SQL编写难题

并且,由于是先设计数据库,肯定也属于 database first 模式

PHP体系的symfony

这个框架历史也比较悠久了,它利用了 Doctrine 找个类库作为orm

利用它之前,也须要先定义模型,然后天生支持文件,然后建表,但是在实际利用的时候,还是和laravel一样,表名,字段名都须要硬编码

$repository = $this->getDoctrine()->getRepository('AppBundle:Product');// query for a single product by its primary key (usually "id")// 通过主键(常日是id)查询一件产品$product = $repository->find($productId);// dynamic method names to find a single product based on a column value// 动态方法名称,基于字段的值来找到一件产品$product = $repository->findOneById($productId);$product = $repository->findOneByName('Keyboard');// query for multiple products matching the given name, ordered by price// 查询多件产品,要匹配给定的名称和价格$products = $repository->findBy( array('name' => 'Keyboard'), array('price' => 'ASC'));

很明显它也会产生SQL编写难题

其余,并不是先设计表,属于 code first 模式

python体系的orm

在python领域,有一个非常著名的框架,叫django, 其余一个比较出名的叫flask, 前者追求大而全,后者追求小而精

python体系的django

django推举的开拓方法,也是先建模型,但是在查询的时候,这建立的模型,基本上毫无用途

res=models.Author.objects.filter(name='jason').values('author_detail__phone','name') print(res) # 反向 res = models.AuthorDetail.objects.filter(author__name='jason') # 拿作者姓名是jason的作者详情 res = models.AuthorDetail.objects.filter(author__name='jason').values('phone','author__name') print(res) # 2.查询书本主键为1的出版社名称和书的名称 res = models.Book.objects.filter(pk=1).values('title','publish__name') print(res) # 反向 res = models.Publish.objects.filter(book__id=1).values('name','book__title') print(res)

如上连表查询的代码,values('title','publish__name') 这里面写的全都是字段名,硬编码进去,进而产生sql语句,查询出结果

很显然,它也会产生SQL编写难题

其余,并不是先设计表,属于 code first 模式

python体系的flask

flask本身没有orm,一样平常搭配 sqlalchemy 利用

利用 sqlalchemy 的时候,一样平常也是先建模型,然后查询的时候,可以直策应用模型的属性,而无须硬编码

result = session. query(User.username,func.count(Article.id)).join(Article,User.id==Article.uid).group_by(User.id).order_by(func.count(Article.id).desc()).all()

如上 Article.id 即是 Article 模型下的 id 属性

很显然,它不会产生SQL编写难题

其余,并不是先设计表,属于 code first 模式

go体系的orm

在go体系,orm比较多,属于百花齐放的形态,比如海内用的多得gorm以及gorm gen,国外比较多的ent, 当然还有我自己写的 arom

go体系下的gorm

利用gorm,一样平常的流程是你先建立模型,然后利用类似如下的代码进行操作

type User struct { Id int Age int}type Order struct { UserId int FinishedAt time.Time}query := db.Table("order").Select("MAX(order.finished_at) as latest").Joins("left join user user on order.user_id = user.id").Where("user.age > ?", 18).Group("order.user_id")db.Model(&Order{}).Joins("join (?) q on order.finished_at = q.latest", query).Scan(&results)

这是一个嵌套查询,虽然定义了模型,但是查询的时候并没有利用模型的属性,而是输入硬编码

很显然,它会产生SQL编写难题

其余,是先设计模型,属于 code first 模式

go体系下的gorm gen

gorm gen 是 gorm 团队开拓的另一款产品,和mybaits下的Mybatis3 Dynamic Sql比较像

它的流程是 先创建数据表,然后利用工具天生构造体(类)和支持代码, 然后再利用天生的构造体

它天生的比较关键的代码如下

func newUser(db gorm.DB) user { _user := user{} _user.userDo.UseDB(db) _user.userDo.UseModel(&model.User{}) tableName := _user.userDo.TableName() _user.ALL = field.NewAsterisk(tableName) _user.ID = field.NewInt64(tableName, "id") _user.Name = field.NewString(tableName, "name") _user.Age = field.NewInt64(tableName, "age") _user.Balance = field.NewFloat64(tableName, "balance") _user.UpdatedAt = field.NewTime(tableName, "updated_at") _user.CreatedAt = field.NewTime(tableName, "created_at") _user.DeletedAt = field.NewField(tableName, "deleted_at") _user.Address = userHasManyAddress{ db: db.Session(&gorm.Session{}), RelationField: field.NewRelation("Address", "model.Address"), } _user.fillFieldMap() return _user}

把稳看,个中大多数代码的浸染是啥?不虞外,便是将构造体的属性与表字段做映射关系

_user.Name 对应 name_user.Age 对应 age

如此,跟mybaits下的Mybatis3 Dynamic Sql的思路非常同等

范例查询代码如下

u := query.Usererr := u.WithContext(ctx). Select(u.Name, u.Age.Sum().As("total")). Group(u.Name). Having(u.Name.Eq("group")). Scan(&users)// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

这是一个分组查询,定义了模型,也利用了模型的属性。

但是呢,它须要利用工具天生额外的支持代码,并且须要先定义数据表

很显然,它不会产生SQL编写难题

其余,它是先设计表,属于 database first 模式

go体系下的ent

ent 是 facebook公司开拓的Orm产品,与 gorm gen 有相通,也有不同

相同点在于,都是利用工具天生实体与数据表字段的映射关系

不同点在于gorm gen先有表和字段,然后天生实体

ent是没有表和字段,你自己手动配置,配置完了一起天生实体和建表

接下来,看一眼ent天生的映射关系

const ( // Label holds the string label denoting the user type in the database. Label = "user" // FieldID holds the string denoting the id field in the database. FieldID = "id" // FieldName holds the string denoting the name field in the database. FieldName = "name" // FieldAge holds the string denoting the age field in the database. FieldAge = "age" // FieldAddress holds the string denoting the address field in the database. FieldAddress = "address" // Table holds the table name of the user in the database. Table = "users")

有了映射关系,利用起来就比较大略了

u, err := client.User. Query(). Where(user.Name("realcp")). Only(ctx)

把稳,这里没有硬编码

它须要利用工具天生额外的支持代码,并且须要先配置表构造

很显然,它不会产生SQL编写难题

其余,它属于先设计表,属于 database first 模式

go体系下的aorm

aorm 是我自己开拓的orm库,吸取了ef core 的一些优点,比较核心的步骤如下

和大多数orm一样,须要先建立模型,比如

type Person struct { Id null.Int `aorm:"primary;auto_increment" json:"id"` Name null.String `aorm:"size:100;not null;comment:名字" json:"name"` Sex null.Bool `aorm:"index;comment:性别" json:"sex"` Age null.Int `aorm:"index;comment:年事" json:"age"` Type null.Int `aorm:"index;comment:类型" json:"type"` CreateTime null.Time `aorm:"comment:创建韶光" json:"createTime"` Money null.Float `aorm:"comment:金额" json:"money"` Test null.Float `aorm:"type:double;comment:测试" json:"test"` }

然后实例化它,并且保存起来

//Instantiation the struct var person = Person{} //Store the struct object aorm.Store(&person)

然后即可利用

var personItem Person err := aorm.Db(db).Table(&person).WhereEq(&person.Id, 1).OrderBy(&person.Id, builder.Desc).GetOne(&personItem) if err != nil { fmt.Println(err.Error()) }

很显然,它不会产生SQL编写难题

其余,它属于先设计模型,属于 code first 模式

总结

本文,我们提出了两个衡量orm功能的原则,并且比拟了几大主流后端措辞的orm,汇总列表如下

框架

措辞

SQL编写难题

code first

额外创建文件

MyBatis 3

java

有难度

不是

须要

MyBatis-Plus

java

有难度

不是

不须要

MyBatis Dynamic SQL

java

没有

不是

须要

jOOQ

java

没有

不是

须要

ef core

c#

没有

不须要

laravel

php

有难度

不是

不须要

symfony

php

有难度

不是

须要

django

python

有难度

不是

不须要

sqlalchemy

python

没有

不须要

grom

go

有难度

不须要

grom gen

go

没有

不是

须要

ent

go

没有

不是

须要

aorm

go

没有

不须要

单就从这张表来说,不考虑其他条件,在做orm技能选型时,

如果你利用java措辞,请选择 MyBatis Dynamic SQL 或者 jOOQ,由于选择他们不会有SQL编写难题

如果你利用c#措辞,请选择 ef core, 这已经是最棒的orm了,不会有SQL编写难题,支持code first,并且不须要额外的事情

如果你利用php措辞,请选择 laravel 而不是 symfony, 反正都有SQL编写难题,那就挑个随意马虎利用的

如果你利用python措辞,请选择 sqlalchemy 库, 不会有SQL编写难题,支持code first,并且不须要额外的事情

如果你利用go措辞,请选择 aorm 库, 不会有SQL编写难题,支持code first,并且不须要额外的事情

标签:

相关文章

PHP实现文字转图片的代码与应用

图片处理技术在各个领域得到了广泛应用。在PHP编程中,文字转图片功能同样具有很高的实用价值。本文将针对PHP实现文字转图片的代码进...

网站建设 2025-03-02 阅读1 评论0

NAN0017探索新型纳米材料的奥秘与应用

纳米技术作为一门新兴的交叉学科,近年来在材料科学、生物医学、电子工程等领域取得了举世瞩目的成果。其中,NAN0017作为一种新型纳...

网站建设 2025-03-02 阅读3 评论0

L26368XO代码其背后的创新与突破

编程语言在各个领域发挥着越来越重要的作用。在众多编程语言中,L26368XO代码以其独特的优势,成为了业界关注的焦点。本文将深入剖...

网站建设 2025-03-02 阅读1 评论0

HTML字体背景打造个化网页设计的关键元素

网页设计已经成为现代网络传播的重要手段。在众多网页设计元素中,字体和背景的搭配尤为关键。本文将从HTML字体背景设置的角度,探讨其...

网站建设 2025-03-02 阅读1 评论0