✌️

Android の Room と Windows の EF Core を比較してみた

2020/10/01に公開

背景

最近、現場で現行アプリ(Android)の Windows 版を新たに別途開発する仕事をしております。
その中で、DB ライブラリの Room で書かれたコードを EF Core に置き換える作業をしました。
それぞれのライブラリの理解を深める良い機会となったので、メモとして残します。
なお、DB の基本やライブラリの導入方法、細かな文法については説明しませんのでご了承ください。

使用したサンプル

以下のサイトの例をサンプルとして使用させていただきました。
Room を使用してローカル データベースにデータを保存する  |  Android デベロッパー  |  Android Developers

エンティティ

まず、こちらが Room(Kotlin) のコードです。

@Entity(tableName = "user")
data class User(
    @PrimaryKey(autoGenerate = true)
    val uid: Int,
    @ColumnInfo(name = "first_name")
    val firstName: String?,
    @ColumnInfo(name = "last_name")
    val lastName: String?
)

次に、こちらが EF Core(C#)のコードです。

[Table("user")]
public class User
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]
    public int uid;
    [Column("first_name")]
    public string firstName;
    [Column("last_name")]
    public string lastName;
}

各ライブラリで書き方の違いはあれど大まかには同じように見えます。
2 つを見比べていただくと何となくわかりますが、それぞれ以下のようになっております。

項目 Kotlin C#
テーブル名定義 @Entity(tableName = "テーブル名") [Table("テーブル名")]
主キー @PrimaryKey [Key]
オートインクリメント (autoGenerate = true) [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
カラム名定義 @ColumnInfo(name = "カラム名") [Column("カラム名")

ちなみにテーブル名およびカラム名について特に何も定義しなければ、いずれのライブラリもクラス名およびフィールド名がそのまま使用されます。

DAO(Repository)

まず、こちらが Room(Kotlin) のコードです。

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>
    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
        "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User
    @Insert
    fun insertAll(vararg users: User)
    @Delete
    fun delete(user: User)
}

次に、こちらが EF Core(C#)のコードです。

public class UserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async ValueTask<List<User>> GetAll()
    {
        return await _context.User.ToListAsync();
    }

    public async ValueTask<List<User>> LoadAllByIds(List<int> ids)
    {
        return await _context.User.Where(u => ids.Contains(u.uid)).ToListAsync();
    }

    public async ValueTask<User> FindByName(string first, string last)
    {
        return await _context.User.Where(u => u.firstName.Equals(first) && u.lastName.Equals(last)).SingleAsync();
    }

    public async ValueTask<int> Delete(User user)
    {
        _context.User.Remove(user);
        return await _context.SaveChangesAsync();
    }

    public async ValueTask<int> InsertAll(List<User> users)
    {
        _context.User.AddRange(users);
        return await _context.SaveChangesAsync();
    }
}

DAO に関しては各ライブラリで実装が大きく異なりました。いろいろ調べた限りだと、EF Core に関しては DAO というか Repository クラスを作るやり方が良さそうでした。EF Core は O/R マッパーなので、やはりそれっぽい書き方になります。また、O/R マッパーは結構書き方の癖が強い感じで正直使いづらく、クエリ文を直接書ける Room のほうが使いやすかったです。

データベース

まず、こちらが Room(Kotlin) のコードです。

@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

次に、こちらが EF Core(C#)のコードです。

public class AppDbContext : DbContext
{
    public DbSet<User> User { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=database-name.db");
    }
}

書き方の違いはあれど、そこまで大幅な違いはありませんでした。
ただ、EF Core のほうはデータベースのバージョンを指定するところがありませんでした。ライブラリ内部でうまいこと管理されているのでしょうか。移行の概要 - EF Core | Microsoft Docsを読んでみると良さそうです。

まとめ

DB ライブラリの Room と EF Core を比較してみました。
そもそもプラットフォームが全く違う 2 つを比較するというのはおかしな話ですが、2 つのライブラリをいっぺんに学習できたりして面白かったです。
それに現行アプリ(Android)の Windows 版を作るというのは、かなり貴重な経験だと感じています。
同じようなことをされている方がいらっしゃるかどうかは分かりませんが、参考になればと。

Discussion