NyxCode / ormx

bringing orm-like features to sqlx
MIT License
287 stars 32 forks source link

Dynamic field updates #12

Closed RoDmitry closed 3 years ago

RoDmitry commented 3 years ago

if Dynamic updates (which can't use Patch) are really a common usecase... - NyxCode

We can use one full update query to compile check, and small generated queries for modified fields.

We must have the struct of all Option fields, with all by default None and changed are Some. Nullable fields are Option<Option<T>>.

Possible use case:

let mut update = User::new_update();
update.name = Some("asdf".to_owned());
update.update(&db);

And this use case might be also possible, but not necessary:

let mut update = User::new_update();
update.set_name("asdf".to_owned());
update.update(&db);

Here we use setters to make the variable of Some<T>. It needs to generate a lot of functions, which might be redundant? But it looks better.

Discussion of it took place in the Discord.

RoDmitry commented 3 years ago

That's my vision on how to make it right. I'm kind of new in Rust, if something is wrong tell me.

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub password: String,
    pub update: Option<UserUpdate>,
}

#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct UserUpdate {
    pub name: Option<String>, // maybe use pointers to String?
    pub password: Option<String>,
}

impl User {
    pub fn set_name(&mut self, name: String) -> &mut User {
        if self.name != name {
            self.name = name.clone(); // avoid clone?
            if let Some(user_update) = &mut self.update {
                user_update.name = Some(name); // maybe use pointer to self.name?
            } else {
                self.update = Some(UserUpdate { ..Default::default() });
                if let Some(user_update) = &mut self.update {
                    user_update.name = Some(name);
                }
            }
        }
        self
    }

    // abstract types
    pub fn update(&mut self, db: &DbType) -> ResultType {
        let result = self.update.update(db);
        if result.success {
            self.update = None;
        }
        result
    }
}

You call user.set_name("asdf".to_owned()) on the User you have, and it will be updated in DB once user.update(&db) is called.

NyxCode commented 3 years ago

I would suggest making the update completely seperate from the actual struct. Here's my vision:

struct User {
    id: i32,
    name: String,
    password: String,
}

let user = User::by_id(&db, 1);
let mut update: UserChangeset = user.changeset();
update.name = Some("name".to_owned());
update.update(&db);

ormx would generate UserChangeset which, in this example, would look like this:

struct UserChangeset {
    name: Option<String>,
    password: Option<String>,
    update: Option<Option<UserUpdate>>,
}
RoDmitry commented 3 years ago

But user.set_name() checks if the name was actually changed, that's easier to work with. And user struct fields will be updated also, so it's synchronized for output to the template for example.

NyxCode commented 3 years ago

The concept of batch-updating should be completely seperate. With regards to updating the fields of the user too, this is still possible. I suggest this API:

struct User {
    id: u32,
    name: String,
    password: String
}

impl User {
    fn changeset(&mut self) -> UserChangeset<'_> {
        UserChangeset {
            _ref: self,
            name: None,
            password: None
        }
    }
}

struct UserChangeset<'a> {
    _ref: &'a mut User,
    name: Option<String>,
    password: Option<String>
}

impl <'a> UserChangeset<'a> {
    fn update(self) {
        if let Some(name) = self.name {
            self._ref.name = name;
        }
        if let Some(password) = self.password {
            self._ref.password = password;
        }
        // write to database
    }
}

then, you can do updates like this:

let mut user = User {
    id: 1,
    name: "John".to_owned(),
    password: "$2y$12$OgdSDmNerIDMqW7N5vO7e.5NjMwPGXoe2zqLFMP8nLKbkUsInQPq.".to_owned()
};

let mut update = user.changeset();
update.name = Some("hey".to_owned());
update.update();
RoDmitry commented 3 years ago

Ok, then can we do the checking of whether User fields were modified or not?

RoDmitry commented 3 years ago

Maybe like that? Now it's bool instead of Option, and we change User fields immediately. If you need old data on error, I would consider cloning before any change, and if update is successful then drop old User. Or use user.reload(&db).

struct User {
    id: u32,
    name: String,
    password: String
}

impl User {
    pub fn changeset(&mut self) -> UserChangeset<'_> {
        UserChangeset {
            _ref: self,
            name: false,
            password: false
        }
    }
}

struct UserChangeset<'a> {
    _ref: &'a mut User,
    name: bool,
    password: bool
}

impl <'a> UserChangeset<'a> {
    pub fn set_name(&mut self, name: String) -> &mut Self {
        if self._ref.name != name {
            self._ref.name = name;
            self.name = true;
        }
        self
    }

    pub fn update(self) { // must return Result
        // write to database
    }
}

fn main() {
    let mut user = User {
        id: 1,
        name: "John".to_owned(),
        password: "$2y$12$OgdSDmNerIDMqW7N5vO7e.5NjMwPGXoe2zqLFMP8nLKbkUsInQPq.".to_owned(),
    };

    let mut changeset = user.changeset();
    changeset.set_name("Admin".to_owned());
    changeset.update();
}
NyxCode commented 3 years ago

closing this due to inactivity If there is something left to say, feel free to comment here and I will reopen the issue, or create a PR.