14 KiB
NOTES
This is a heavily opinionated logbook I keep here until there is something usable.
Please keep in mind, that I sometimes rant in writing, but learn later that I was wrong.
It was not supposed to be read by anyone else than me.
SeaORM
Some Rant about ORM Implementations
Entity, ActiveModel, Model
SeaORM tries to implement (those #@!$) DDD ideas in much detail, creating this multitude of fixed struct names, like user::Model, user::Entity, user::ActiveModel, where each operation still needs us to import the Trait itself, most code is hidden behind macros, and you still end up having to call "insert" on the active model, instead of a repository object.
At least the activemodel, unlike in implementations like Django's ActiveRecord pattern, seems to support dirty flags, given you have to define fields as enums "Set()" and "NotSet" in the model. This probably will make it easier to save models without creating huge SQLs, or micromanaging which fields are touched, or resaving data that was actually unchanged, allowing more transparent PATCH implementations.
When it comes to writing ORM Code, SeaORM is rather bulky, and complicated, somehow the opposite of what I expect from an ORM implementation. At least however, it is concise, and you can access all your model parts, like Columns, by the same pattern, and allow nice filter patterns (e.g. user::Column.Name.contains(...)) that could be expanded by implementing additional functions on them.
So, once one accepts the concept, it works mostly well, and as it forces you with the tools to separate database concerns from your main code base, you are tempted to write your own repositories and value objects for interacting with the storage layer.
Why functionality implemented on Entity could not have been implemented for Model instead, I am not sure. Also, having access to the Model and ActiveModel type from Entity, would be at least easier, if you could just pub use table::Entity as MyModelName
, and access MyModelName::Model
, or MyModelName::ActiveModel
, respectively, instead of sometimes using the module name, and then alternating between Entity and Model.
It also kind of sucks, to have Entity as a name, as imho the more prevalent use of that word is in ECS systems, where it kind of means the same thing, but not really, while in the context of DDD Storage mechanisms, pardon my hot take, it is just a waste of good nomenclature.
I have yet to find out, how i can move the automatically generated entity
and migration
folders to some subdirectory, like "crates", given putting them in the workspace is rather daunting, and is not a nice thing in general, imho, if not adjustable, but I am fairly sure that works out somehow.
I also don't see the reason, why I would separate the migration app from the entity app, and not manage the whole thing in one layer instead, given I have to now look for version changes of sea-orm in 3 Cargo.tomls for 1 project. It seems to be an overabstraction, that is dictated by the tooling, mainly sea-orm-cli.
However, this may be mitigated, or may be "accepted" by the user, as it is only a problem for a certain point of idealism, and might be easily defended in a discussion by anyone, who thinks that layers are things you cannot have enough of, and tons of Uncle Bob quotes.
Source of Truth
In Diesel my main issue was, that the source of truth was never clear for models, as the tools rewrote the schema.rs, while the migration could also be generated from changing the schema itself. Here SeaORM clearly shows 2 approaches, either migration first, or entity first. However, where Diesel seems to grow into a better workflow, and started to have autodetection of migration changes, even generating a migration (instead of an entity), from a current table seems unsupported in SeaORM, which means, either you accept writing migrations manually, and using that as your state of truth, or you lose all migration support.
The entity first approach, is, as expected, not the main mindset.
Ease of Use and the ORM Idealism
Most ORMs come with some idealisms behind it. For example, if you look at SQLAlchemy in the Python world, sometimes you have the feeling, the authors never really wanted to write an ORM, as you need some SQL statements at least, usually to set up databases. I would say, Diesel is very similar in that mindset, as they only later introduced a programmatical migration language, and instead, expect you to write SQL in their tutorial. Django may have it's flaws, but the ORM of Django clearly has a source of truth: the Model. Changing the Model leads to auto-detected migrations, which means, you can automate checking model changes in the CI/CD. Also, the model language of django nearly covers all aspects of SQL, except the DEFAULT value, which seems to be hard to implement, as most ORMs don't support it.
SeaORM seems like a weird cross-over. While I would have said, Diesel tries to go into the direction of full automation, and might one day finally solve it's source of truth issue, if it finally starts to introduce more options to define models, and throws away the idea of a common generated schema file, SeaORM seems not to bother, as probably most users of it work with the migration workflow in mind, and "there is is this a documented way to create entities in the database from code", even if that never really ties into their migration syntax.
Both ORMs force you to put your models in certain places, even if SeaORM is more flexible if you just don't use sea-orm-cli, while diesel just won't run otherwise, and therefore expect the database layer to be some global service layer, which is fine in a microservice world, but kind of sucks in a modular monolith. So putting database models in various applications, like you might be used from Django, is not really a thing.
It is better to just see your storage layer as a global database layer, and implement local value objects that implement From for the storage layer objects, and some repositories, that do all the ORM work behind the curtain.
It's not like that is a bad thing, given this is also one of the downsides I witness in django projects, where models start to become swiss army knives around a domain topic.
Accepting My Fate
Generating Entities from a Database
sea-orm-cli generate entity -u postgresql://miniweb:miniweb@localhost:54321/miniweb -o entity/src --lib
Admin Registry
Dynamic index functions
Dynamic Repo Traits
trait AdminRepository {
type Item;
type List;
fn get_item(&self, id: usize) -> Option<Self::Item>;
fn get_list(&self) -> Self::List;
}
struct AdminRegistry {
repositories: HashMap<String, Box<dyn Any>>,
}
impl AdminRegistry {
fn register<R: AdminRepository + 'static>(&mut self, name: &str, repository: R) {
self.repositories.insert(name.to_string(), Box::new(repository));
}
}
struct UserRepository;
impl AdminRepository for UserRepository {
type Item = User;
type List = Vec<User>;
fn get_item(&self, id: usize) -> Option<User> {
// Retrieve a single user
}
fn get_list(&self) -> Vec<User> {
// Retrieve a list of users
}
}
Finding Names
Ultimately the goal will be to rename even "app" and "model" as concepts later on - maybe.
As I started by quickly using a django admin template with CSS to jump start the admin interface, with the hope to create something that is very much also compatible with existing django admin templates in projects, I ran quickly into naming maelstrom.
Django-Admin:
key | description |
---|---|
app_label | This refers to the name of the Django application that contains the model. It's usually set to the name of the directory that holds the app. In Django's admin interface and internal workings, app_label is used to distinguish models from different apps that might have the same model name. |
model_name | This is the name of the model class in lowercase. Django uses model_name as a convenient way to refer to models, particularly in URL patterns and in the database layer, where the table name is typically derived from the app_label and model_name. |
object_name | object_name is the name of the model class itself, typically starting with a capital letter (following Python's class naming conventions). This is more often used in Django's internals and templates. |
verbose_name | This is a human-readable name for the model, which can be set in the model's Meta class. If not set explicitly, Django generates it automatically by converting the object_name from CamelCase to space-separated words. verbose_name is used in the Django admin interface and any place where a more readable model name is needed. |
verbose_name_plural | Similar to verbose_name, this is the plural form of the human-readable name of the model. It's used in the Django admin interface where a plural form of the model's name is appropriate, like in list views. |
miniweb-admin:
key | description |
---|---|
app_key | refers to the lowercase slug representation of the app, which might be represented by an Arc in the future |
model_key | refers to the lowercase slug representation of the model, which might be represented by an Arc in the future |
app_name | represents the human readable representation of the app |
model_name | represents the human readable representation of the model |
Replacing:
django-admin | miniweb-admin | explanation |
---|---|---|
object_name|lower | key, app_key, model_key | |
app_label | key, app_key |
Next Steps:
- implement views for app
- implement a static implementation for repository
- eventually AdminRegistry needs to become an
Arc<Mutex<T>>
, if internal data needs to change after creating the main state.
CSS Library Brainhurt
Bootstrap
Everybody says it is evil. I am not sure why yet.
Pico.css
maybe it would be better to go for something simple?
UiKit
in general really nice, but I got annoyed by uk- prefixes pretty fast.
Psychologically, "ui grid" seems nicer than "uk-grid", weird.
It would however seem to align with hx- prefixes of htmx?
Tailwind/DaisyUI
Tailwind supposed to make you uber designer.
Strongly suggested by professionals like theo/t3, however I kind of started to see everything he says as an anti-pattern. Jonathan, you are rubbing off.
I kind of want less "class"es not more. I understand why tailwind might be good. But then again, I don't get why I should decorate each tag with classes.
Semantic/Fomantic
Semantic seems aligned with the idea of htmx and hyperscript a lot in it's philosophy of naming things.
It comes with jQuery basement, which is also a good recall to how a htmx site might work in the end.
Not having access to the main repo since 2 years, and being developed by the community is both a red flag and a good sign: there is an active community, but hampered potential.
I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use.
Why fomantic won was the lack of needing a btn class.
Builder Pattern etc.
consider builder: https://docs.rs/derive_builder/latest/derive_builder/ downside of builders as macro: no intellisense!
//#[derive(Builder)]
//#[builder(setter(into))]
static initializer
downside of a pub &'static str initializer struct is the rule to avoid any nested types that have any kind of building pattern, because you cannot properly move the data easily, especially if you want to move into enums, like an Option.
otherwise, self mutating building patterns can stay readonly either with the Copy trait and using self:
With "Copy": fn self_mutating(mut self, value) -> Self { self.value = value; self }
Without Copy: fn self_mutating_manual(self, value) -> Self { Struct { value: value, ..self } }
However also works, but seems dirty: fn self_mutating_instance_mut(&mut self, value) -> Self { self.value = value; *self }
Cow
Other remarks: also useful is the usage of Cow, if you want to create internal objects that actuall reuse static data or take dynamic data.
use std::borrow::Cow;
#[derive(Debug, Serialize, Clone)] pub struct Field { pub widget: Cow<'static, str>, pub label: Option<Cow<'static, str>>, }
let static_field = Field::widget(Cow::Borrowed("/admin/widgets/input_text.jinja")); let dynamic_label = String::from("Dynamic Label"); let dynamic_field = Field::widget(Cow::Owned(dynamic_label));
Write article about "did you read the fineprint"
- The bleeding of lifetimes.
- 'static
- Send
- "object safe"
- the async dynamic dispatch.
- the building pattern debacle here
Async Trait Adventure
What was not noted here was the few days gone by researching async Trait dilemmas. Given I use dynamic dispatch, the only solution is async_trait crate for now However I am aware that this is an ongoing topic in the rust world.
For the admin this works well, and in general, it makes sense in the axum world I guess.
Other solutions if this stops being the case:
- Channels like in Go with crossbeam: https://gsquire.github.io/static/post/a-rusty-go-at-channels/
- Watch Rust Development here: https://rust-lang.github.io/async-fundamentals-initiative/index.html
For the occasional HTMX Reload on Error bug
The header being in the history, returns a partial that cannot insert itself into the browser's error page.
Maybe a hint for a cache control problem actually? https://github.com/bigskysoftware/htmx/issues/854
Make axum log requests!
There is another crate, that uses some MIME detection middleware for cache-control, called axum-cc, which could inspire middlewares here.
Changing Rust Toolchain on Windows
change to GNU:
rustup toolchain install stable-x86_64-pc-windows-gnu
change back to MSVC:
rustup toolchain install stable-x86_64-pc-windows-msvc
or:
rustup toolchain install stable-msvc
activate toolchain with rustup default
udeps
https://github.com/est31/cargo-udeps
While compilation of this tool also works on Rust stable, it needs Rust nightly to actually run.
cargo install cargo-udeps --locked
cargo +nightly udeps
switching to nightly and back
rustup override set nightly