--- title: "Relations" description: "Define and query relationships between entities." canonical: "https://roomsharp.dev/docs/v0.5.4/relations" source: "src/content/v0.5.4/relations.mdx" --- # Relations RoomSharp supports relationships between entities through the `[Relation]` attribute, `[Junction]` metadata, and embedded types. ## Relation Basics Use `[Relation]` to define parent-child relationships between entities: ```csharp [Entity(TableName = "users")] public class User { [PrimaryKey(AutoGenerate = true)] public long Id { get; set; } public required string Name { get; set; } } [Entity(TableName = "posts")] public class Post { [PrimaryKey(AutoGenerate = true)] public long Id { get; set; } [ForeignKey(typeof(User))] public long AuthorId { get; set; } public required string Title { get; set; } } // Result type combining both public class UserWithPosts { [Embedded] public required User User { get; set; } // Expose the parent key at this container level so RelationLoader can read it. public long Id => User.Id; [Relation(Entity = typeof(Post), ParentColumn = nameof(Id), EntityColumn = nameof(Post.AuthorId))] public List Posts { get; set; } = []; } ``` ## Loading Relations Generated DAO query methods can return DTO/projection types. Relation properties are then loaded explicitly with `WithRelationsAsync`, or through QueryExtensions `Include(...)`: ```csharp [Dao] public interface IUserDao { [Query("SELECT * FROM users WHERE Id = :id")] Task GetUserWithPostsAsync(long id); [Query("SELECT * FROM users ORDER BY Name")] Task> GetAllUsersWithPostsAsync(); } var user = await db.UserDao.GetUserWithPostsAsync(id); if (user is not null) { await user.WithRelationsAsync(db); } ``` `RelationLoader` uses batched secondary queries for relation properties. QueryExtensions `Include(...)` uses the same relation metadata when you want fluent loading from `db.From()`. ## Many-to-Many Relations For many-to-many relationships, use a junction table: ```csharp [Entity(TableName = "users")] public class User { [PrimaryKey(AutoGenerate = true)] public long Id { get; set; } public required string Name { get; set; } } [Entity(TableName = "groups")] public class Group { [PrimaryKey(AutoGenerate = true)] public long Id { get; set; } public required string Name { get; set; } } [Entity(TableName = "user_groups")] public class UserGroup { [ForeignKey(typeof(User))] public long UserId { get; set; } [ForeignKey(typeof(Group))] public long GroupId { get; set; } } // Result types public class UserWithGroups { [Embedded] public required User User { get; set; } public long Id => User.Id; [Relation( Entity = typeof(Group), ParentColumn = nameof(Id), EntityColumn = nameof(Group.Id) )] [Junction( Value = typeof(UserGroup), ParentColumn = nameof(UserGroup.UserId), EntityColumn = nameof(UserGroup.GroupId) )] public List Groups { get; set; } = []; } ``` ## Embedded Types Use `[Embedded]` to include all columns from another type inline: ```csharp public class Address { public string Street { get; set; } = ""; public string City { get; set; } = ""; public string Country { get; set; } = ""; } [Entity(TableName = "users")] public class User { [PrimaryKey(AutoGenerate = true)] public long Id { get; set; } public required string Name { get; set; } [Embedded(Prefix = "home_")] public Address HomeAddress { get; set; } = new(); [Embedded(Prefix = "work_")] public Address WorkAddress { get; set; } = new(); } ``` The table would have columns: `Id`, `Name`, `home_Street`, `home_City`, `home_Country`, `work_Street`, `work_City`, `work_Country`. ## Relation Attributes | Attribute | Description | |-----------|-------------| | `Entity` | Related entity type. If omitted, RoomSharp infers it from the relation property type. | | `ParentColumn` | Column on the parent entity to match | | `EntityColumn` | Column on the child entity to match | | `AssociateBy` | Simple junction table name for many-to-many relations | | `[Junction].Value` | Junction entity type for many-to-many relations | | `[Junction].ParentColumn` | Junction column referencing parent | | `[Junction].EntityColumn` | Junction column referencing related entity | ## Performance Considerations Loading deep relation hierarchies can result in many database round-trips. Consider: - Using projections to select only needed columns - Breaking complex relations into separate queries - Using `NoTracking = true` for read-only scenarios For complex queries involving multiple relations, consider using `RoomSharp.QueryExtensions` with explicit JOINs for better control over query execution. --- ## RelationLoader `RoomSharp.Relations.RelationLoader` can populate relation properties after a query finishes. Extension methods `WithRelationsAsync` exist for single entities and lists: ```csharp public class UserWithPosts { [Embedded] public User User { get; set; } = default!; // Expose key used by RelationLoader public long Id => User.Id; [Relation(Entity = typeof(Post), ParentColumn = "Id", EntityColumn = "UserId")] public List Posts { get; set; } = new(); } var user = await db.UserDao.GetByIdAsync(id); var hydrated = await user.WithRelationsAsync(db); ``` --- ## Paging & LiveData `RoomSharp.Paging` offers `PagingSource`, `Pager`, and helper DTOs for incremental loading using callbacks that RoomSharp DAOs can feed. ```csharp var pagingSource = new PagingSource(async (page, size, ct) => { // Delegate to DAO using LIMIT/OFFSET query return await db.TodoDao.GetPageAsync(page * size, size, ct); }); await foreach (var page in new Pager(pagingSource).GetPagesAsync()) { Render(page); } ``` ### LiveData `RoomSharp.Core.LiveData` provides observer-based in-memory streams. It works well together with paging or relation loaders for simple reactive scenarios. ```csharp var liveData = new LiveData(() => db.TodoDao.GetActiveCountAsync().GetAwaiter().GetResult()); liveData.Observe(count => Console.WriteLine($"Active todos: {count}")); await liveData.RefreshAsync(); ``` For more advanced reactive scenarios, see the [Reactive Queries](/docs/v0.5.4/reactive-queries) page.