-
Notifications
You must be signed in to change notification settings - Fork 2
Triggers
A trigger runs SQL inside the database when a row is inserted, updated or deleted. SQLite runs every trigger once per changed row, so the body can read the affected row through OLD and NEW. There are no statement-level triggers in SQLite.
The framework gives you three ways to work with triggers. A raw SQL body, a typed LINQ body that is checked at compile time and model triggers that are created and migrated together with the table.
db.Schema.CreateTrigger<T>(...) creates a trigger on the table for T. The body and the optional WHEN predicate are raw SQL strings. Use OLD and NEW to refer to the row.
await db.Schema.CreateTriggerAsync<Book>(
name: "trg_book_history",
timing: SQLiteTriggerTiming.After,
@event: SQLiteTriggerEvent.Update,
body: "INSERT INTO BookHistory(BookId, OldPrice, NewPrice) VALUES (NEW.BookId, OLD.BookPrice, NEW.BookPrice)",
when: "OLD.BookPrice <> NEW.BookPrice");
await db.Schema.DropTriggerAsync("trg_book_history");SQLiteTriggerTiming is Before, After or InsteadOf. SQLiteTriggerEvent is Insert, Update or Delete. InsteadOf only works on Views.
A second CreateTrigger overload builds the body from LINQ instead of a SQL string. Column names and the WHEN guard are checked at compile time and values are translated to SQL the same way queries are.
await db.Schema.CreateTriggerAsync<Book>("trg_book_history", SQLiteTriggerTiming.After, SQLiteTriggerEvent.Update, t => t
.When(() => t.Old.Price != t.New.Price)
.Insert(db.Table<BookHistory>(), s => s
.Set(h => h.BookId, _ => t.New.Id)
.Set(h => h.OldPrice, _ => t.Old.Price)
.Set(h => h.NewPrice, _ => t.New.Price)));The builder works like this:
-
t.Oldis the row before the change, valid forUPDATEandDELETEtriggers.t.Newis the row after the change, valid forINSERTandUPDATEtriggers. They map to SQLite'sOLDandNEWrows and are only valid inside the expressions passed to the builder. -
When(() => ...)sets the trigger'sWHENguard, so the body runs only for rows where the predicate is true. It can be called once per trigger. - Each
Update,InsertorDeletecall adds one statement to the body, so one trigger can run several statements. -
Update(target, predicate, setters)updates rows in another table. The predicate and the setter values can read the target row andt.Old/t.New. -
Insert(target, values)inserts one row. EachSetpairs a target column with a value expression. -
Delete(target, predicate)deletes matching rows.
CreateTrigger creates the trigger right away and is not tracked by the model. To make a trigger part of the model, declare it with Trigger(...) in OnModelCreating. Model triggers are created by CreateTable and a TableChanged migration creates them when they are missing and recreates them when their body changes. Triggers that are not declared on the model are left alone.
Inside OnModelCreating, reach the target table through the database's own Table<TTarget>(), which is in scope.
protected override void OnModelCreating(SQLiteModelBuilder builder)
{
builder.Entity<Book>()
.Trigger("trg_Book_Audit", SQLiteTriggerTiming.After, SQLiteTriggerEvent.Insert, t => t
.Insert(Table<AuditLog>(), s => s.Set(a => a.BookId, _ => t.New.Id)));
}Record every price change with the old and new value. The trigger writes the history row in the same transaction as the update, so the log cannot miss a change or record one that was rolled back.
builder.Entity<Book>()
.Trigger("trg_Book_PriceHistory", SQLiteTriggerTiming.After, SQLiteTriggerEvent.Update, t => t
.When(() => t.Old.Price != t.New.Price)
.Insert(Table<PriceHistory>(), s => s
.Set(h => h.BookId, _ => t.New.Id)
.Set(h => h.OldPrice, _ => t.Old.Price)
.Set(h => h.NewPrice, _ => t.New.Price)));Keep a count column on the parent in step with its child rows, so reads never need the aggregate.
builder.Entity<OrderItem>()
.Trigger("trg_OrderItem_CountUp", SQLiteTriggerTiming.After, SQLiteTriggerEvent.Insert, t => t
.Update(Table<Order>(), o => o.Id == t.New.OrderId, s => s
.Set(o => o.ItemCount, o => o.ItemCount + 1)))
.Trigger("trg_OrderItem_CountDown", SQLiteTriggerTiming.After, SQLiteTriggerEvent.Delete, t => t
.Update(Table<Order>(), o => o.Id == t.Old.OrderId, s => s
.Set(o => o.ItemCount, o => o.ItemCount - 1)));The counter is updated by the database itself, so it stays correct no matter which code path writes the child table.
You do not need to write these by hand. An external-content FTS5 table declared with AutoSync = FtsAutoSync.Triggers gets the standard insert, update and delete sync triggers generated for you. See Full Text Search. To change the generated trigger shapes, override the trigger methods on a SQLiteSchema subclass, described under customizing schema generation on the Schema page.
- A trigger fires for every row the statement changes, including rows changed by bulk operations like
ExecuteUpdateandExecuteDelete. - By default a trigger's own writes do not fire other triggers recursively. Turn that on with the
RecursiveTriggerspragma, see Pragmas. - There is no update-of-columns filter in the API. Use a
Whenguard comparingt.Oldandt.Newto react to specific column changes. -
DropTrigger(name)removes a trigger by name. Dropping a table drops its triggers with it.