以前別の記事で少し言及したのですが、その件についてとりあえずわかった範囲で調べたので書きます。

環境など


環境とかです。

  • .NET Framework 4.8
  • EntityFramework core 2.2.4
  • Connector/NET for Entity Framework 8.0.13

接続先のDBはAmazon Aurora MySQLです。

コード


まず、コードをみてください。下記のようなBlogAuthorというクラスがあるとします。これらのクラスはDBのテーブルとマップされます。

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
public class BlogDbContext: DbContext
{
Public DbSet<Blog> Blog { get; set;}
}

public class AuthorDbContext: DbContext
{
Public DbSet<Author> Author { get; set;}
}

[Table("blog")]
public class Blog
{
[Column("id")]
public int Id { get; set; }

[Column("author_id")]
public int AuthorId { get; set; }

[Column("url")]
public string Url { get; set; }

[Column("title")]
public string Title { get; set; }

[Column("content")]
public string Content { get; set; }

[Column("created_at")]
public DateTime CreatedAt { get; set; }

[Column("updated_at")]
public DateTime? UpdatedAt { get; set; }
}

[Table("author")]
public class Author
{
[Column("id")]
public int Id { get; set; }

[Column("name")]
public string Name { get; set; }
}

public class Result
{
public int AuthorName { get; set; }
public string Title { get; set; }
public string Content { get; set; }

public Result(int _authorName, string _title, string _content)
{
this.AuthorName = _authorName;
this.Title = _title;
this.Content = _content;
}
}

そして、これらのテーブルをJOINしてSelectしたものをResultというクラスに放り込みたいとします。ここでは更新があった(updated_atがnullでない)Blogのタイトルと内容と著者名を取得したいとします。以下のようなコードので動くはずです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (var blogDbContext = new BlogDbContext())
using (var authorDbContext = new AuthorDbContext())
{
var query =
blogDbContext.Blog
.Where(b => b.UpdatedAt != null)
.Join(
inner: authorDbContext.Author,
outerKeySelector: blog => blog.AuthorId,
innerKeySelector: author => author.id,
resultSelector: (blog, author) => new Result(author.Name, blog.Title, Blog.Content)
)

return query.ToList();
}

動かないんだな…これが…


しかし、前述のコードはArgumentNullExceptionが発生してしまいます。

私も(まさか有名なライブラリでこんなバグ?があるのが)いまだに信じられません。これは本当にバグなんでしょうか?ただ、もっとIssueが上がっていてもおかしくないので、特定条件下だけなのではないかとおもいます。例えばMySQLの時だけなどです。

3.0.0ではFixされているらしい…のだが…


上記のIssueは3.0.0のマイルストーンに入っており、マージ済みなので3.0.0(現時点ではPreview)を使用すればよいのでは?ということで3.0.0-previewを使用したのですが、今度はConnector/NET for Entity Framework 8.0.13が3.0.0-previewに対応していないのでDBに接続に行く段階でコケます。

ToListする


しょうがないのでstack overflowの回答を参考にしてToListをつけて実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using (var blogDbContext = new BlogDbContext())
using (var authorDbContext = new AuthorDbContext())
{
var query =
blogDbContext.Blog
.Where(b => b.UpdatedAt != null)
.ToList()
.Join(
inner: authorDbContext.Author.ToList(),
outerKeySelector: blog => blog.AuthorId,
innerKeySelector: author => author.id,
resultSelector: (blog, author) => new Result(author.Name, blog.Title, Blog.Content)
)

return query.ToList();
}

これで正しく取得できるにはできます。が、これはSQLのJOINではなくコレクションのJOINになるようで、レコードが多いとタイムアウトでコケます。

※途中で落ちるのと時間がなかったのでどういった理由で落ちるのか正確に調べられてないのですが、いったんBlogテーブルの条件に一致するレコードを取得してそれを基にAuthorのテーブルからレコードを引っ張ってきているような感じ(要するにJOINではなくWHERE INで取得しようとしてるっぽい)でした。

生クエリ(RawSQL)


詰んだかほりがするのですが、あきらめるわけにもいきません。今回は外部から不正なパラメータを入力されることはなく、SQLインジェクションの心配もないので生のSQLを書くことにしました。まず、取得した結果を格納するためのモデルとそれ用にDbContextを継承したクラスを書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ResultDbContext: DbContext
{
Public DbSet<Result> Result { get; set;}
}

public class Result
{
[Column("name")]
public int AuthorName { get; set; }

[Column("title")]
public string Title { get; set; }

[Column("content")]
public string Content { get; set; }
}

後はこれらを使ってJOINして結果取得します。

1
2
3
4
5
6
7
8
9
10
11
12
using (var resultDbContext = new ResultDbContext())
{
var query =
resultDbContext.Result.FromSql(
"SELECT author.name, blog.title, blog.content" +
" FROM blog" +
" INNER JOIN author ON blog.author_id = author.id" +
" WHERE blog.updated_at is null"
);

return query.ToList();
}

これで一応ちゃんと動きます。

所感


今でもこれはバグではなくて私がどこか実装でミスしているのではないかという気がしてならないのですが、そういったIssueがあがっている以上はやはりバグなんでしょうか??しかしこれは大変厳しい…