前言
最近一段時間,我使用golang開發了一個新的ORM庫。
為了讓這個庫更好用,我比較研究了各語言的主流ORM庫,發現有一些語言的ORM庫确實很好用,而有另外一些語言的庫那不是一般的難用。
然後我總結了他們呢的一些共性和差異點,于是形成了本文的主要内容。
本文會先說明什麼是SQL編寫難題,以及探讨一下 code first 和 database first 的優缺點。 然後依據這兩個問題的結論去審視目前主流後端語言java, c#, php, python, go各自的orm庫,對比研究下他們的優缺點。最後給出總結和參考文檔。
如果你需要做技術選型,或者做技術研究,或者類似于我做架構開發,或者單純地了解各語言的差異,或者就是想吹個牛,建議儲存或收藏。如果本文所涉及到的内容有任何不正确,歡迎批評指正。
溫馨提示,本文會有一些戲谑或者調侃成分,并非對某些語言或者語言的使用者有任何歧視意見。 如果對你造成了某些傷害,請多包涵。
什麼是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.User
err := 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,并且不需要額外的工作