ORM哪家強?java,c#,php,python,go 逐一對比, 網友直呼:全面客觀

語言: CN / TW / HK

前言

最近一段時間,我使用golang開發了一個新的ORM庫。

為了讓這個庫更好用,我比較研究了各語言的主流ORM庫,發現有一些語言的ORM庫確實很好用,而有另外一些語言的庫那不是一般的難用。

然後我總結了他們呢的一些共性和差異點,於是形成了本文的主要內容。

本文會先説明什麼是SQL編寫難題,以及探討一下 code firstdatabase first 的優缺點。 然後依據這兩個問題的結論去審視目前主流後端語言java, c#, php, python, go各自的orm庫,對比研究下他們的優缺點。最後給出總結和參考文檔。

如果你需要做技術選型,或者做技術研究,或者類似於我做框架開發,或者單純地瞭解各語言的差異,或者就是想吹個牛,建議保存或收藏。如果本文所涉及到的內容有任何不正確,歡迎批評指正。

温馨提示,本文會有一些戲謔或者調侃成分,並非對某些語言或者語言的使用者有任何歧視意見。 如果對你造成了某些傷害,請多包涵。

renzhen.png

什麼是SQL編寫難題

如果你是做web開發,那麼必然需要保存數據到數據庫,這個時候你必須熟悉使用sql語句來讀寫數據庫。

sql本身不難,命令也就那幾個,關鍵字也不算多,但是為什麼編寫sql會成為難題呢?

比如下面的sql ```sql select * from user

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

`` 它有什麼難題? 簡單的單表操作嘛,一點難題沒有,但凡學過點sql`的程序員都能寫出來,並且保證正確。我估計比例能超過90%

但是,如果你需要寫下面的sql呢? 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? 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相關的代碼,不會有語法提示,也不會有表名提示,字段名提示。

最終的代碼質量如何全憑你的眼力,經驗,能力。

下載.png

很顯然,既然存在該難題,那麼哪個ORM能解決該難題,就應該算得上好,如果不能解決,則不能稱之為好。

什麼是code first 和 database first

這倆概念並不是新概念,但是我估計大多數開發者並不熟悉。

所謂 code first, 相近的詞是 model fist, 意思是模型優先,指的是在設計和開發系統時,優先和重點做的工作是設計業務模型,然後根據業務模型去創建數據庫。

所謂 database first,意思是數據庫優先,指的是在設計和開發系統時,優先和重點做的工作是創建數據庫結構,然後去實現業務。

這裏我提到了幾個詞語,可能在不同的語言裏叫法不一樣,可能不同的人的叫法也不一樣,為了下述方便,我們舉例子來説。

code first 例子

假設我是一個對電商系統完全不懂的小白,手頭上也沒有如何設計電商系統的資料,我和我的夥伴只是模糊地知道電商系統主要業務就是處理訂單。

然後我大概會知道這個訂單,主要的信息包括哪個用户下單,什麼時間下單,有哪幾種商品,數量分別是多少,根據這些已有的信息,我可以設計出來業務模型如下

java public class OrderModel { //訂單編號 Integer orderId; //用户編號 Integer userId; //訂單時間 Integer createTime; //訂單詳情(包含商品編號,商品數量) String orderDetail; } 很簡單,對吧,這個模型很匹配我目前對系統的認知。接下來會做各種業務邏輯,最後要做的是將訂單模型的數據保存到數據庫。但是在保存數據到數據庫的時候,就有一些考慮了。

我可以將上面OrderModel業務模型建立一張對應表,裏面的4個屬性,對應數據表裏的4個字段,這完全可以。 但是我是電商小白,不是數據庫小白啊,這樣存儲的話,肯定不利於統計訂單商品的。

所以我換一種策略,將OrderModel的信息進行拆分,將前三個屬性 orderId, userId, createTime 放到一個新的類裏。 然後將 orderDetail 的信息進行再次分解,放到另一個類裏 ```java 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文件內容如下 ```xml insert into user (id,name,mobile) values (#{id},#{name},#{mobile})

<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>

`` 你在編寫這個xml文件的時候,這個手寫sql沒有本質區別,一定會遇到剛才説到的SQL編寫難題`。

Java體系的Mybatis-plus

這裏有必要提一下 Mybatis-plus,它是國內的團隊開發出來的工具,算是對Mybatis的擴展吧,它減少了xml文件內容的編寫,減少了一些開發的痛苦。比如,你可以使用如下的代碼來完成以上相同的工作 ```java 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語句是程序運行時生成的,而不是提前寫好的,或者定義好的。

它的使用流程是,先在數據庫裏定義好數據表,然後創建模型文件,讓然後通過命令行工具,將每一個表生成如下的支持文件 ```java public final class PersonDynamicSqlSupport { public static final Person person = new Person(); public static final SqlColumn id = person.id; public static final SqlColumn firstName = person.firstName; public static final SqlColumn lastName = person.lastName; public static final SqlColumn birthDate = person.birthDate; public static final SqlColumn employed = person.employed; public static final SqlColumn occupation = person.occupation; public static final SqlColumn 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項目裏的類裏面的屬性,做了一一映射。

接下來你在開發的時候,就不用關心表名,以及字段名了,直接使用剛才生成的類,以及類下面的那些屬性。具體如下

```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);

``` 如上面的代碼,好處有以下四點

  1. 你不再需要手寫sql
  2. 也不用在意字段名了,因為使用的都是類,或者屬性,編寫代碼的時候編輯器會有提示,編譯的時候如果有錯誤也會提示,實際運行的時候就不會有問題了。
  3. 聯表查詢,嵌套查詢啥的,也都支持
  4. 完美避開了SQL編寫難題

當然帶來了額外的事情,比如你要使用工具來生成PersonDynamicSqlSupport類,比如你要先建表。

先建表這事兒,很明顯就屬於 database first 模式。

事情不大.png

C#體系的orm

C# 在工業領域,遊戲領域用的多一些,在web領域少一些。

它也有自己的orm,名字叫 Entity Framework Core, 一直都是微軟公司在維護。

下面是一個典型的聯表查詢

c# 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來説,你可以腦補一下下面的場景

javavsc#.png

PHP體系的orm

php體系內,框架也非常多,比如常見的laravel,symfony,這裏我們就看這兩個,比較有代表性

PHP體系的laravel

使用php語言開發web應用的也很多,其中比較出名的是laravel框架,比較典型的操作數據庫的代碼如下 php $user = DB::table('users')->where('name', 'John')->first(); 這裏沒有使用模型(就算使用了也差不多),代碼裏出現的 users 就是數據庫表的名字, name 是 users 表裏的字段名,他們是被直接寫入代碼的

很明顯它會產生SQL編寫難題

並且,因為是先設計數據庫,肯定也屬於 database first 模式

PHP體系的symfony

這個框架歷史也比較悠久了,它使用了 Doctrine 找個類庫作為orm

使用它之前,也需要先定義模型,然後生成支持文件,然後建表,但是在實際使用的時候,還是和laravel一樣,表名,字段名都需要硬編碼 ```php $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 模式

下載.png

python體系的orm

在python領域,有一個非常著名的框架,叫django, 另外一個比較出名的叫flask, 前者追求大而全,後者追求小而精

python體系的django

django推薦的開發方法,也是先建模型,但是在查詢的時候,這建立的模型,基本上毫無用處 ```python 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 的時候,一般也是先建模型,然後查詢的時候,可以直接使用模型的屬性,而無須硬編碼 python 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 模式

下載 (1).png

go體系的orm

在go體系,orm比較多,屬於百花齊放的形態,比如國內用的多得gorm以及gorm gen,國外比較多的ent, 當然還有我自己寫的 arom

go體系下的gorm

使用gorm,一般的流程是你先建立模型,然後使用類似如下的代碼進行操作 ```go 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比較像

它的流程是 先創建數據表,然後使用工具生成結構體(類)和支持代碼, 然後再使用生成的結構體

它生成的比較關鍵的代碼如下 ```go 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的思路非常一致

典型查詢代碼如下 ```go 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生成的映射關係 go 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" ) 有了映射關係,使用起來就比較簡單了 go u, err := client.User. Query(). Where(user.Name("realcp")). Only(ctx) 注意,這裏沒有硬編碼

它需要使用工具生成額外的支持代碼,並且需要先配置表結構

很顯然,它不會產生SQL編寫難題

另外,它屬於先設計表,屬於 database first 模式

go體系下的aorm

aorm 是我自己開發的orm庫,吸取了ef core 的一些優點,比較核心的步驟如下

和大多數orm一樣,需要先建立模型,比如 ``go type Person struct { Id null.Intaorm:"primary;auto_increment" json:"id"Name null.Stringaorm:"size:100;not null;comment:名字" json:"name"Sex null.Boolaorm:"index;comment:性別" json:"sex"Age null.Intaorm:"index;comment:年齡" json:"age"Type null.Intaorm:"index;comment:類型" json:"type"CreateTime null.Timeaorm:"comment:創建時間" json:"createTime"Money null.Floataorm:"comment:金額" json:"money"Test null.Floataorm:"type:double;comment:測試" json:"test"` }

然後實例化它,並且保存起來go //Instantiation the struct var person = Person{}

//Store the struct object
aorm.Store(&person)

然後即可使用go 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 模式

下載 (2).png

總結

本文,我們提出了兩個衡量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,並且不需要額外的工作

好了,文章寫兩天了,終於寫完了。如果對你有幫助,記得點贊,收藏,轉發。

如果我有説的不合適,或者不對的地方,請在下面狠狠的批評我。

微信圖片_20221226163643.png

參考文檔

MyBatis 3
MyBatis-Plus
MyBatis Dynamic SQL
jOOQ: The easiest way to write SQL in Java
Entity Framework Core 概述 - EF Core | Microsoft Learn
數據庫和Doctrine ORM - Symfony開源 - Symfony中國 (symfonychina.com)
Django(ORM查詢、多表、跨表、子查詢、聯表查詢) - 知乎 (zhihu.com)
Sqlalchemy join連表查詢_FightAlita的博客-CSDN博客_sqlalchemy 連表查詢
Gorm + Gen自動生成數據庫結構體_Onemorelight95的博客-CSDN博客_gorm 自動生成
tangpanqing/aorm: Operate Database So Easy For GoLang Developer (github.com)