Part 2. Entity Framework Core 1.0 の基本的な使い方
では引き続き、Entity Framework Core の利用方法を解説していきます。
■ 基本的な Entity Framework Core の利用方法
従来の Entity Framework と異なり、EF Core では O/R マッパーファイル(*.dbml)を使うことができません。このため、O/R マッピング(データベーステーブルのどこを構造体クラスのどこにマッピングするのか?)はすべてコードで指定する必要があります。ツールを利用して自動生成させることも(ある程度は)可能ですが、現時点(2016/07/02)では、手で書いてしまったほうがやりやすいと思います。
まずは以下 2 つのファイルを用意します。
- データベースファイル
- プロジェクトファイル直下に App_Data フォルダを掘り、pubs.mdf ファイルを置きます。
- データベースファイルは公開する必要がないため、wwwroot 下に置く必要はありません。私は昔の名残で App_Data という名前を使っていますが、この名前である必要性も特にありません。お好みで変えていただいて結構です。
- モデルファイル
- プロジェクトファイル直下に Models フォルダを切り、Pubs.cs ファイルを作成します。
Pubs.cs ファイルに、データを取り出すための構造体クラスを記述し、そこに O/R マッピング情報を記述していきます。今回は、全テーブルをやるとキリがないので、以下 6 つのテーブルについてだけ取り出してマッピングしてみます。
- authors : 著者データ
- titles : 書籍データ
- titleauthor : 著者と書籍の多対多中間テーブル
- publishers : 出版社データ
- stores : 店舗データ
- sales : 売上データ
最終的なコードは以下の通りです(※ OnConfiguring() メソッド内のパス情報は適宜書き換えてください)。えらい長いコードで恐縮ですが;、順番に説明していきます。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace Decode2016.WebApp.Models
{
public partial class PubsEntities : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(
@"data source=(LocalDB)\mssqllocaldb;attachdbfilename=|DataDirectory|\pubs.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"
.Replace("|DataDirectory|", @"C:\Users\nakama\Documents\Visual Studio 2015\Projects\Decode2016.WebApp\src\Decode2016.WebApp\App_Data"));
}
public DbSet<Author> Authors { get; set; }
public DbSet<Title> Titles { get; set; }
public DbSet<Publisher> Publishers { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Sale> Sales { get; set; }
public DbSet<TitleAuthor> TitleAuthors { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Sale>().HasKey(s => new { s.StoreId, s.OrderNumber, s.TitleId });
modelBuilder.Entity<TitleAuthor>().HasKey(ta => new { ta.AuthorId, ta.TitleId });
modelBuilder.Entity<Sale>().HasOne(s => s.Title).WithMany(t => t.Sales).IsRequired();
modelBuilder.Entity<Sale>().HasOne(s => s.Store).WithMany(s => s.Sales).IsRequired();
modelBuilder.Entity<Publisher>().HasMany(p => p.Titles).WithOne(t => t.Publisher).IsRequired();
modelBuilder.Entity<Author>().HasMany(a => a.TitleAuthors).WithOne(ta => ta.Author).IsRequired();
modelBuilder.Entity<Title>().HasMany(t => t.TitleAuthors).WithOne(ta => ta.Title).IsRequired();
}
}
[Table("authors")]
public partial class Author
{
[Column("au_id"), Required, MaxLength(11), Key]
public string AuthorId { get; set; }
[Column("au_fname"), Required, MaxLength(20)]
public string AuthorFirstName { get; set; }
[Column("au_lname"), Required, MaxLength(40)]
public string AuthorLastName { get; set; }
[Column("phone"), Required, MaxLength(12)]
public string Phone { get; set; }
[Column("address"), MaxLength(40)]
public string Address { get; set; }
[Column("city"), MaxLength(20)]
public string City { get; set; }
[Column("state"), MaxLength(2)]
public string State { get; set; }
[Column("zip"), MaxLength(5)]
public string Zip { get; set; }
[Column("contract"), Required]
public bool Contract { get; set; }
[Column("rowversion"), Timestamp, ConcurrencyCheck]
public byte[] RowVersion { get; set; }
public ICollection<TitleAuthor> TitleAuthors { get; set; }
}
[Table("publishers")]
public partial class Publisher
{
[Column("pub_id"), Required, MaxLength(4)]
public string PublisherId { get; set; }
[Column("pub_name"), MaxLength(40)]
public string PublisherName { get; set; }
[Column("city"), MaxLength(20)]
public string City { get; set; }
[Column("state"), MaxLength(2)]
public string State { get; set; }
[Column("country"), MaxLength(30)]
public string Country { get; set; }
public ICollection<Title> Titles { get; set; }
}
[Table("titles")]
public partial class Title
{
[Column("title_id"), Required, MaxLength(6), Key]
public string TitleId { get; set; }
[Column("title"), Required, MaxLength(80)]
public string TitleName { get; set; }
[Column("type"), Required, MaxLength(12)]
public string Type { get; set; }
[Column("price")]
public decimal? Price { get; set; }
[Column("advance")]
public decimal? Advance { get; set; }
[Column("royalty")]
public int? Royalty { get; set; }
[Column("ytd_sales")]
public int? YeatToDateSales { get; set; }
[Column("notes"), MaxLength(200)]
public string Notes { get; set; }
[Column("pubdate"), Required]
public DateTime PublishedDate { get; set; }
[Column("pub_id"), MaxLength(4)]
public string PublisherId { get; set; }
public Publisher Publisher { get; set; }
public ICollection<Sale> Sales { get; set; }
public ICollection<TitleAuthor> TitleAuthors { get; set; }
}
[Table("sales")]
public partial class Sale
{
// ※ 複合キーは Data Annotation で指定できないため、Fluent API を使う
[Column("stor_id"), Required, MaxLength(4)]
public string StoreId { get; set; }
[Column("ord_num"), Required, MaxLength(20)]
public string OrderNumber { get; set; }
[Column("ord_date"), Required]
public DateTime OrderDate { get; set; }
[Column("qty"), Required]
public int Quantity { get; set; }
[Column("payterms"), Required, MaxLength(12)]
public string PayTerms { get; set; }
[Column("title_id"), Required, MaxLength(6)]
public string TitleId { get; set; }
public Store Store { get; set; }
public Title Title { get; set; }
}
[Table("stores")]
public partial class Store
{
[Column("stor_id"), Required, MaxLength(4), Key]
public string StoreId { get; set; }
[Column("stor_name"), Required, MaxLength(40)]
public string StoreName { get; set; }
[Column("stor_addr"), Required, MaxLength(40)]
public string Address { get; set; }
[Column("city"), Required, MaxLength(20)]
public string City { get; set; }
[Column("state"), Required, MaxLength(22)]
public string State { get; set; }
[Column("zip"), Required, MaxLength(5)]
public string Zip { get; set; }
public ICollection<Sale> Sales { get; set; }
}
[Table("titleauthor")]
public partial class TitleAuthor
{
[Column("au_id"), Required]
public string AuthorId { get; set; }
[Column("title_id"), Required]
public string TitleId { get; set; }
[Column("au_ord")]
public byte AuthorOrder { get; set; }
[Column("royaltyper")]
public int RoyaltyPercentage { get; set; }
public Author Author { get; set; }
public Title Title { get; set; }
}
}
■ コードの構造について
上記のソースの各クラスの役割は以下の通りです。
- PubsEntites
- データベース接続を管理するクラス。DbContext クラスから派生させて作成する。
- この接続下で取り扱うテーブル一覧もここに記述される。
- 接続文字列の指定方法は複数あるが、基本的には OnConfiguring メソッド内で指定する。
- EF Core では SQL Server 以外にも接続できるため、SQL Server に接続したい場合には、project.json ファイルで Microsoft.EntityFrameworkCore.SqlServer ライブラリを組み込んだ上で、options.UseSqlServer(…) として接続先を指定する。
- Author, Publisher, Title, Sale, Store, TitleAuthor クラス
- データベースの各テーブルに対応させて作成した構造体クラス。
- データベース上の 1 レコードが、これらのオブジェクトの 1 インスタンスに読みだされる。
- このため、データベース上のテーブル名は複数形、C# のクラス名は単数形になるのが一般的。
- 属性(クラス定義やプロパティ定義の上につけられたカギカッコ)により、データベースとのマッピング方法を指定する。
- ほとんどのマッピングは属性で指定できるが、複合キーやリレーションシップに関する情報は(現時点では)属性では指定できない。このような場合には、PubsEntities クラスの OnModelCreating() メソッド内でコードで O/R マッピング情報を指定する。
- 属性を使って O/R マッピングを指定する方法をデータアノテーション方式、OnModelCreating() メソッド内でコードを使って O/R マッピングを指定する方法を Fluent API 方式と呼ぶ。
おおまかなコードの構造は以下の通りです。(リレーションシップの指定などに関しては少し重要なので、後述します。)
public partial class PubsEntities : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// ここに接続文字列を書く
}
// ここにテーブル一覧を書く
public DbSet<Author> Authors { get; set; }
public DbSet<Title> Titles { get; set; }
public DbSet<Publisher> Publishers { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Sale> Sales { get; set; }
public DbSet<TitleAuthor> TitleAuthors { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ここにデータアノテーションで指定できない O/R マッピング情報を書く
}
}
[Table("authors")]
public partial class Author
{
[Column("au_id"), Required, MaxLength(11), Key]
public string AuthorId { get; set; }
[Column("au_fname"), Required, MaxLength(20)]
public string AuthorFirstName { get; set; }
...
}
[Table("publishers")]
public partial class Publisher
{
[Column("pub_id"), Required, MaxLength(4)]
public string PublisherId { get; set; }
...
}
...
■ リレーションシップの O/R マッピング方法について
リレーションシップについては、以下のように実装します。
- 1 : 多について
- 例えば、出版社(Publisher)と書籍(Title)は 1 : 多の関係になりますが、これは以下のように実装します。
- Publisher クラス側
- 1 つの Publisher に複数の Title が紐づけられるので、ICollection<Title> Titles プロパティを作成します。
- Title クラス側
- 1 つの Title には 1 つの Publisher が紐づくので、Publisher Publisher プロパティを作成します。
- PublisherId プロパティは持っても持たなくても構いませんが、通常はあると便利なので持たせてしまいます。
- 上記準備を済ませたうえで、リレーションシップに関する O/R マッピング情報を OnModelCreating() メソッドに記述します。
[Table("publishers")]
public partial class Publisher
{
[Column("pub_id"), Required, MaxLength(4)]
public string PublisherId { get; set; }
[Column("pub_name"), MaxLength(40)]
...
public ICollection<Title> Titles { get; set; }
}
[Table("titles")]
public partial class Title
{
[Column("title_id"), Required, MaxLength(6), Key]
public string TitleId { get; set; }
[Column("title"), Required, MaxLength(80)]
public string TitleName { get; set; }
...
[Column("pub_id"), MaxLength(4)]
public string PublisherId { get; set; }
public Publisher Publisher { get; set; }
}
modelBuilder.Entity<Publisher>().HasMany(p => p.Titles).WithOne(t => t.Publisher).IsRequired();
- 多 : 多について
- 例えば、著者(Author)と書籍(Title)は、多 : 多テーブルである TitleAuthor テーブルを介して 多 : 多 の関係を持ちます。
- このような多 : 多の関係については、代表的には以下の 2 つの方式での O/R マッピングが考えられます。
- ① 直接、多 : 多の関係をオブジェクトで表現する
- Author オブジェクトのプロパティとして ICollection<Title> Titles を、Title オブジェクトのプロパティとして ICollection<Author> Authors を持つが…
- 中間テーブルである TitleAuthor に対応するオブジェクトは作らない
- ② 中間テーブルまで含めてオブジェクトで表現する
- 中間テーブルである TitleAuthor に対応するオブジェクトを明示的に作り、Author オブジェクトのプロパティとして ICollection<TitleAuthor> TitleAuthors を、Title オブジェクトのプロパティとして ICollection<TitleAuthor> TitleAuthors を持たせる
- すなわち、2 組の 1 : 多 の関係であるとして表現してしまう
- ① 直接、多 : 多の関係をオブジェクトで表現する
- どちらにもメリット・デメリットがありますが、一般的には②方式の方が幅広く利用できます。
- 今回のサンプルのように、著者(Author)と書籍(Title)の間の TitleAuthor に印税料(Royalty)などの属性がついている場合には、明確にオブジェクトとして作成する必要があります。
- ①の方式の場合には、単に 1 : 多の関係を 2 つ作成するだけになります。
■ LINQ クエリの記述
O/R マッピングファイルができたら、あとは LINQ クエリを実行します。今回は簡単のため、Startup.cs の app.Run() 内を書き換えて実行してみましょう。
app.Run(async (context) =>
{
using (PubsEntities pubs = new PubsEntities())
{
var query = from a in pubs.Authors where a.State == "CA" select a;
await context.Response.WriteAsync(query.Count().ToString());
}
});
正しく動作すれば、15 件という結果が帰ってくるはずです。
なお、EF で必要となる LINQ クエリの記述方法についてはここでは説明しませんが、LINQ クエリは EF を扱う上での必須技術であるため、まだ知らないという方は必ず学習してください。拙著「LINQ テクノロジ入門」は EF の前進となる LINQ to SQL という技術を使って書かれていますが、LINQ クエリの書き方そのものは基本的に変わりません。こちらの本をざっと流し読みしていただければ、LINQ の基本的な考え方などは学習できると思います。
■ 従来の EF からの大きな変更点について
(ここはちょっと難しいのでわかる人だけ読んでください) Entity Framework Core では様々な変更が入っていますが、実用側面から見た場合、以下の 2 つは非常に大きな変更点ですので、ここで解説しておきます。
Lazy Loading の廃止
従来の EF では、リレーションシップの先にあるデータを、クエリ実行後に後から手繰れるというトンデモ仕様が含まれていました。現場側の人間からすると、誰だこの学術的機能を入れた人は;、と全力でツッコミたかったわけですが、EF Core ではこの仕様が廃止されました。このため、以下のクエリは実行時に例外が発生します。
もちろん、場合によっては「クエリ実行時に、リレーションシップの先までデータを取得しておいてほしい」ということもあるはずです。この場合には、クエリ発行前に、明示的に Include, ThenInclude 命令で取り込む対象を指定してください。
非同期処理
従来の EF では、ToList() や FirstOrDefault() などによるクエリ実行は同期的にしか実行できませんでしたが、こうしたクエリ実行命令に、非同期処理版が追加されました。このため、以下のようなクエリは await/async 構文を利用して、以下のように記述できるようになりました。
var query = from a in pubs.Authors where a.State == "CA" select a;
var result = query.ToList();
↓
var result = await query.ToListAsync();
EF のこのような機能拡張に合わせて、ASP.NET MVC のアクションメソッドや Web API でも、async 構文を使った定義ができるようになりました。
[HttpGet]
public ActionResult ShowTitlesByPublishers()
{
using (PubsEntities pubs = new PubsEntities())
{
var query = pubs.Publishers....;
ViewData["TitlesByPublisher"] = await query.ToList();
}
return View();
}
↓
[HttpGet]
public async Task<ActionResult> ShowTitlesByPublishers()
{
using (PubsEntities pubs = new PubsEntities())
{
var query = pubs.Publishers....;
ViewData["TitlesByPublisher"] = await query.ToListAsync();
}
return View();
}
この新機能については様々なところでよく紹介されていますが、クライアント側(UI 処理)における async/await とは意味合いがずいぶん違うので注意してください。ざっくり説明すると、以下の通りです。
- クライアント側における async/await 処理
- UI スレッド(メインスレッド)上で時間がかかる処理をしてしまうと、「UI が固まる」という現象が起こる。
- この問題を起こさないように、長時間(だいたい 30 msec よりも長い時間)を要する処理を別スレッドに切り出して行うために、async/await を使う。
- サーバ側における async/await 処理
- サーバ側は、通常、下図のようなマルチスレッド処理で複数のユーザの処理を捌いている。
- この際、データアクセスのように時間のかかる処理をしてしまうと、当該スレッドは待機状態となり、CPU が遊んでしまう。
- async/await 処理を行って、CPU を明示的に他のスレッドに回すことで、より CPU の利用効率を高めることができる。
サーバサイドではもともとマルチスレッドで処理が行われているため、「サーバが固まる」という現象が起こるわけではないですし、仮に async/await を明示的に行わなかったとしても、OS のマルチスレッド制御機能により、自動的に CPU リソースが他のスレッドに回されますので、すぐさま問題が起こるというわけではありません。ただ、ASP.NET ランタイムが Windows OS 以外でも動作するという話になってくると、OS によってはこのマルチスレッド制御の機能が貧弱なケースも考えられ、そのような場合には「明示的なリソース解放」を行わないと性能が出ない、というケースが出てくるかもしれません。正直、今どきの OS であればそうそう問題が起こるケースはないだろうとは思いますが、とはいえお作法としては、async/await 処理をきちんと書いた方が、環境依存のトラブルが出にくくなるという意味では安心です。
いずれにしても、サーバ側の async/await は、クライアント側の async/await とは利用目的が違う、という点は知っておくとよいでしょう。
■ 接続文字列の管理方法について
先の例では、接続文字列をハードコーディングしましたが、実際のアプリではいくつか課題があります。解決策をいくつかここで示しておきます。
- アプリが配置されているディレクトリの自動解決
- 従来の ASP.NET で利用していた |DataDirectory| 文字列は残念ながら ASP.NET Core では利用できません。Startup.cs ファイル内であれば、ソースコードのフォルダ名を比較的簡単に解決できます。これを用いて、Startup.cs から PubsEntities.cs にデータを引き渡すとよいでしょう。
- 具体的には以下の通りです。
- Startup.cs にコンストラクタを作る。コンストラクタの引数に IHostingEnvironment を付けておくと、自動的にホスティング環境の情報を渡してくれます。(詳細はここでは解説しませんが、コンストラクタインジェクションと呼ばれる ASP.NET ランタイムの DI コンテナ機能によるものです。)
- さらに、Pubs.cs ファイル側でこれを拾うようにコードを修正します。
- ※ (つぶやき)本来を言えば PubsEntities 側でパスを自動解決するように実装したいのですが、1.0.0 RTM 版の時点では PlatformServices クラスに ApplicationEnvironment プロパティしかなく、HostingEnvironment プロパティが存在しないため、そのような実装ができません。将来的には PlatformServices クラスから解決できるようになるのではないかと思います。
public class Startup
{
public static string App_Data { get; set; }
public Startup(IHostingEnvironment env)
{
App_Data = Path.Combine(env.ContentRootPath, "App_Data");
}
...
public partial class PubsEntities : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(
@"data source=(LocalDB)\mssqllocaldb;attachdbfilename=|DataDirectory|\pubs.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"
.Replace("|DataDirectory|", Startup.App_Data));
}
...
- 開発環境/運用環境の切り替え
- #if DEBUG 文を差し込んでおき、開発環境と運用環境の設定を切り替えます。
#if DEBUG
options.UseSqlServer(
@"data source=(LocalDB)\mssqllocaldb;attachdbfilename=|DataDirectory|\pubs.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"
.Replace("|DataDirectory|", Startup.App_Data));
#else
options.UseSqlServer(
@"Server=tcp:xxxxxxxx.database.windows.net,1433;Database=pubs;User ID=xxxxxxxx@xxxxxxxx;Password=xxxxxxxx;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;");
#endif
なお、ASP.NET Core では構成設定値を web.config ファイルに書きません。この点は従来の ASP.NET Web Forms から大きく変わっている点ですのでご注意ください。背景は以下の通りです。
- 変更になった最大の理由は、従来の ASP.NET Web Forms では web.config の設定が肥大化し、メンテナンスがつらくなってしまったため。
- 従来の web.config ファイルには、大別して以下の 2 つの設定情報が書かれたが、ASP.NET Core では、これらの情報を分離して扱う。
- ① ASP.NET ランタイムパイプラインのカスタマイズ方法や設定
- Startup.cs ファイルの ConfigureServices(), Configure() の 2 つのメソッド内にコードとして記述する。
- ConfigureServices() で DI コンテナの設定を、Configure() でパイプラインの設定をする。
- ② 接続文字列などの純粋な設定値
- 単純なものや、セキュリティ上の問題がないものであれば、上記の例のようにソースコード中にハードコードしてしまうのがラク。
- どうしてもファイルに切り出したい場合などは、ASP.NET Core の新しい構成設定システムを利用する。(ASP.NET Core の新しい構成設定システムについてはこのエントリでは解説しないので、こちらを参考にしてください。)
- ① ASP.NET ランタイムパイプラインのカスタマイズ方法や設定
■ その他の制約事項・注意事項について
その他、現在の Entity Framework Core 1.0 に関する注意点は以下の通りです。
- Azure の SQL Database に対する利用について
- Windows Azure の PaaS データベースである SQL Database に対してクエリを実行する場合、スロットリング(流量制限)によりクエリ実行が失敗するケースがあります。このため、SQL Database に対してクエリを実行する場合には、自動リトライ処理を入れるのがベストプラクティスになっています。
- EF6 ではこの自動リトライ処理の組み込みが簡単に行えましたが、EF Core 1.0 ではまだこれが実装されていません(2016/07/02 時点)。近いうちに実装されるでしょうが、現時点では制約事項と考えておく必要があります。(ちょっと自力で作り込むのは大変;)
- 自動トランザクションとの組み合わせについて
- ASP.NET Core では、System.Transactions 配下の TransactionScope が利用できません。Windows プラットフォームを前提とできる場合には、ランタイムとして Full CLR を利用することで TransactionScope の利用もできなくはないですが、その場合でもあまり組み合わせて利用することはオススメはしません。
- これはそもそも Enity Framework と自動トランザクションは、設計として相性が悪いためです。
- Entity Framework では、基本的に、データベースの入出力とは、エンティティというデータの塊の出し入れである、と発想を持っており、「エンティティ」という単位を超えるトランザクション制御はほとんどないよね? という思想を持っています。もちろん実際の業務を見るとエンティティという単位を超えるトランザクション制御が必要になる場合も存在するのですが、そういうものはごく一部だし、高速性を要求する処理だったりすることが多いので、ストプロで実装しちゃってね、という割り切り思想を持っています。(よいか悪いかはともかく)
- 一方、自動トランザクションは、そうした割り切り型の思想はなく、むしろ「開発者の都合次第でいかようにも組んでよい」という、Entity Framework よりも低水準の技術です。このため、自動トランザクションと Entity Framework を組み合わせようとすると、「UPDLOCK ロックヒントが簡単につけられない」などの問題に突き当たることになります。
- このため、EF Core を使う場合、(少なくとも現時点では)以下のようにするのがオススメです。(ロックヒントなどの問題は、中長期的には解決されてくる問題かもしれませんが、現時点では以下のように考えておくとよいと思います。)
- 自動トランザクションとは組み合わせないこと。
- どうしてもトランザクション制御が必要になるところは、ストプロで実装する。
- あまりにもストプロ実装が増えそうなら、EF Core ではなく、生の System.Data.SqlClient などを利用することを検討してみる。
以上が EF Core 1.0 の基本的な使い方になります。その他、より詳しい情報については、de:code 2016 DEV-003 セッション 「新しく生まれ変わったデータアクセステクノロジ~Entity Framework Core 1.0 の全貌~」などを見ていただくとよいと思います。
Comments
- Anonymous
December 26, 2016
pubs.mdf っていきなり出てきましたが、何か特別に作る必要がありますか?実行するとpubs.mdfにDBヘッダがないみたいなエラーページが表示されます。- Anonymous
February 26, 2017
dbのバージョンが合わないので例外が出ます。SQL Sserver 2014で動かすには、https://www.youtube.com/watch?v=iKVbx5IeUvQにその解決方法があります。それと、Author クラスに、RowVersion プロパティが定義されていますが、これは不要だと思います。
- Anonymous