예제 #1
0
    def test(self):
        uri = "/uri/1/"
        with Transaction():
            instance = Dummy()
            with ForceChanger(instance):
                instance.uri = uri
            assert instance.uri_hash == compute_hash_uuid(uri)
            instance.save()
            assert instance.uri_hash == compute_hash_uuid(uri)
        assert instance.uri_hash == compute_hash_uuid(uri)
        mjson1 = instance.mjson

        with ReadonlyTransaction():
            instance = Dummy.objects.get(id=instance.id)
            assert instance.mjson == mjson1
            assert instance.uri == uri
            assert instance.uri_hash == compute_hash_uuid(uri)
예제 #2
0
 def __init__(self,
              default=None,
              check=None,
              null=True,
              normalize=None,
              expire=Expire.NONE,
              create_only=False,
              alias=None):
     super().__init__(
         "data",
         str,
         unique=True,
         default=default,
         check=check,
         filter=lambda cls, v:
         {"computed_uri_hash": compute_hash_uuid(cls.convert_uri(v))},
         null=null,
         normalize=normalize,
         expire=expire,
         create_only=create_only,
         alias=alias,
     )
예제 #3
0
 def compute_uri_hash(self):
     return compute_hash_uuid(self.uri)
예제 #4
0
class Model(AbstractModel, metaclass=ModelBase):
    UNIQUE_KEY_SUBFIELD_NAMES = ("id", "uname", "uri", "uri_hash", "computed_uri_hash")
    PSEUDO_KEY_SUBFIELD_NAMES = ("name",)

    class Meta:
        base_manager_name = "objects"
        unique_together = (
            ("status", "type", "id"),
            ("computed_owner", "status", "type", "id"),
            ("computed_container", "status", "type", "id"),
            ("computed_proprietor", "status", "type", "id"),
        )
        indexes = (GinIndex(fields=("computed_search_array",)),)

    # class part
    objects = ModelManager()
    my_type = CachedClassProperty("_get_my_type", is_freeze=False)
    types = CachedClassProperty("_get_types")
    super_types = CachedClassProperty("_get_super_types")
    subfield_defaults = CachedClassProperty("_get_subfield_defaults")
    required_filters = ({"status", "type"},)
    uri_format = CachedClassProperty("_get_uri_format", is_freeze=False)
    field_names_needs_object_setter = CachedClassProperty("_get_field_names_needs_object_setter")
    is_agent = False

    # system field
    type = IntegerField(null=False)
    optimistic_lock_count = IntegerField(null=False)

    # json field
    data = JSONField(null=False, blank=True)
    computed = JSONField(null=False, blank=True)
    raw = JSONField(null=True, blank=True)

    # data subfields
    created_date = ValueSubfield("data", datetime)
    uname = UnameSubfield()  # intrinsic key (mutable)
    name = ValueSubfield("data", str)  # loose key (mutable)

    # computed subfields
    uri = ValueSubfield(
        "computed",
        str,
        check=lambda v: v[:5] == "/uri/" and v[-1] == "/",
        filter=lambda cls, v: {"computed_uri_hash": compute_hash_uuid(v)},
    )
    uri_hash = ValueSubfield("computed", UUID, filter=lambda cls, v: {"computed_uri_hash": v})

    # relational subfields
    creator = ForeignKeySubfield("data", Type.Actor, create_only=True)
    owner = ForeignKeySubfield("data", Type.Actor)
    container = ForeignKeySubfield("data", Type.Model)
    elements = ReverseForeignKeySubfield("computed", Type.Model, "container")
    proprietor = ForeignKeySubfield("data", Type.Model)
    properties = ReverseForeignKeySubfield("computed", Type.Model, "proprietor")

    # computed field
    computed_uri_hash = UUIDField(unique=True, null=True)  # derived from uname (intrinsic key)
    computed_owner = ForeignKey(
        "Model", related_name="computed_possessions", null=True, on_delete=PROTECT, db_index=False
    )
    computed_container = ForeignKey(
        "Model", related_name="computed_elements", null=True, on_delete=PROTECT, db_index=False
    )
    computed_proprietor = ForeignKey(
        "Model", related_name="computed_properties", null=True, on_delete=PROTECT, db_index=False
    )
    computed_search_array = ArrayField(BigIntegerField(), null=True)

    # class member variable
    _is_initialized = False  # instance member variable 이나 __init__() 전에 setattr 이 호출될 수 있어 세팅함

    @cached_property
    def intrinsic_content_subfield_names(self):
        return tuple(
            k
            for k, v in self.subfields["_total"].items()
            if not isinstance(v, ForeignKeySubfield)
            and not isinstance(v, ReverseForeignKeySubfield)
            and not isinstance(v, SubfieldWrapper)
            and k not in ("created_date", "uname", "uri", "uri_hash")
        )

    def is_same_content(self, other):
        if self.my_type != other.my_type:
            return False
        for subfield_name in self.intrinsic_content_subfield_names:
            if getattr(self, subfield_name) != getattr(other, subfield_name):
                return False
        return True

    def __init__(self, *args, **kwargs):
        assert not self.__class__.__dict__.get("ABSTRACT", False), "인스턴스화가 불가능한 모델입니다."
        self._is_initialized = False
        self._force_changer_count = 0
        super().__init__(*args, subfield_kwargs=kwargs)

    def init_variables(self):
        super().init_variables()
        self._is_initialized = True
        self._mjson_revert_patch = {"data": {}, "computed": {}}
        self._old_uri = None
        if settings.IS_UNIT_TEST:
            self._mjson_on_init = deepcopy(self.mjson)

    @property
    def last_transaction_date(self):
        return get_datetime_from_key(self.last_transaction)

    @classmethod
    def _get_my_type(cls):
        return getattr(Type, cls.__name__)

    @classmethod
    def _get_types(cls):
        my_type = cls.my_type
        if my_type == Type.Model:
            return []
        sub_model_types = list(my_type.sub_types)
        return [my_type] + sub_model_types

    @classmethod
    def _get_super_types(cls):
        return [parent.my_type for parent in cls.mro()[1:] if issubclass(parent, Model)]

    @classmethod
    def _get_subfield_defaults(cls):
        total = {"data": {}, "computed": {"_mjson_revert_patch": None}}
        for k, v in cls.subfields["data"].items():
            total["data"][k] = json_encode(v.default)
        for k, v in cls.subfields["computed"].items():
            total["computed"][k] = json_encode(v.default)
        return total

    @classmethod
    def _get_uri_format(cls):
        app_name = cls.__module__.split(".")[0]
        cls_name = cls.__name__
        header = "/uri/{}/{}/".format(app_name.lower(), cls_name.lower())
        return header + "{}/"

    @classmethod
    def _get_field_names_needs_object_setter(cls):
        return cls.subfield_names + cls.model_level_field_names + cls.only_column_names

    def __str__(self):
        return "({}, {}, {}, {}, {})".format(
            self.type, self.id, self.uri or self.uname or self.name, self.status.name, id(self)
        )

    def _pre_create(self):
        super()._pre_create()
        my_type = self.my_type
        assert my_type is not None, "Type 이 정의되어 있어야 합니다."
        self.type = my_type
        default = json_deepcopy_with_callable(self.subfield_defaults)
        self.data = default["data"]
        self.computed = default["computed"]
        self.optimistic_lock_count = 0
        self.data["created_date"] = str(now())

    def _init_subfields(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    def on_create(self):
        super().on_create()
        for subfield_name, subfield in self.subfields["data"].items():
            encoded = self.data[subfield_name]
            if encoded and not callable(encoded):
                self.onchange_subfield("data", subfield_name, None, json_decode(encoded, subfield.subfield_type))

    def _init_no_inited_subfields(self):
        for k, v in self.data.items():
            if v is None and k[0] != "_":
                subfield = self.get_subfield(k)
                assert subfield.null, console_log("{} 는 null 이 허용되지 않습니다.".format(subfield.subfield_name))
            elif callable(v):
                self.data[k] = json_encode(safe_call(v, self))
        for k, v in self.computed.items():
            if v is None and k[0] != "_":
                subfield = self.get_subfield(k)
                assert subfield.null
            elif callable(v):
                self.computed[k] = json_encode(safe_call(v, self))

    def _pre_syncdb_insert(self, tran):
        super()._pre_syncdb_insert(tran)
        self._init_no_inited_subfields()

    def take_optimistic_lock(self):
        if self.status in (Status.CREATING, Status.NEW):
            return
        with ForceChanger(self):
            self.optimistic_lock_count += 1
        self.save()

    # 절대 함부로 사용하지 말 것. 정말 특수한 경우가 아니라면 활용할 일이 없음
    def _set_fields(self, **kwargs):
        # version
        assert "version" not in kwargs
        old_version = self.version
        self.__dict__["version"] = old_version + 1
        kwargs["version"] = old_version + 1
        # set fields
        for k, v in kwargs.items():
            self.__dict__[k] = v
            if settings.IS_UNIT_TEST:
                self._mjson_on_init[k] = v
        # DB
        result = self.__class__.objects.filter(id=self.id, version=old_version).update(**kwargs)
        if result == 0:
            raise OptimisticLockException
        assert result == 1

    def set_working(self):
        assert self.is_in_writable_transaction(), "해당 instance 가 수정 가능한 Transaction 내에 없습니다."
        if self.status == Status.NEW:
            tran = TransactionManager.get_transaction()
            self._syncdb_insert(tran)
        assert self.status in (Status.NORMAL, Status.DIRTY)
        self._set_fields(status=Status.WORKING)

    def rollback_working(self):
        assert self.status is Status.WORKING, console_log("Status 가 WORKING 일때만 호출이 가능합니다.")
        self._set_fields(status=Status.NORMAL)

    def take_exclusive_lock(self, nowait=False):
        if self.status in (Status.CREATING, Status.NEW):
            return
        manager = self.__class__.objects
        queryset = manager.select_for_update(nowait=nowait).filter(id=self.id)
        assert queryset._result_cache is None or len(queryset._result_cache) == 0
        queryset._fetch_all()
        assert len(queryset._result_cache) == 1

    @classmethod
    def from_db_impl(cls, db, field_names, values):
        t = Type(values[field_names.index("type")])
        # override cls
        cls_t = t.model
        # almost same with DjangoModel.from_db()
        if len(values) != len(cls_t._meta.concrete_fields):
            values_iter = iter(values)
            values = [next(values_iter) if f.attname in field_names else DEFERRED for f in cls_t._meta.concrete_fields]
        new = cls_t(*values)
        new._is_initialized = False
        new._state.adding = False
        new._state.db = db
        # for only Model
        assert new.type == cls_t.my_type
        new.type = Type(new.type)
        new.status = Status(new.status)
        new.init_variables()
        new.patch_json_field()
        return new

    # TODO : schema 만 바뀐 경우에 save() 가 가능하도록 하는 구현 부분 리팩토링
    def patch_json_field(self):
        if self.status == Status.DELETED:
            return
        assert self.status in (Status.NORMAL, Status.WORKING), self.status
        total_default = self.subfield_defaults
        _schema_changed = False
        old_status = self.__dict__["status"]
        for json_field_name in total_default:
            json_field = self.__dict__[json_field_name]
            field_default = total_default[json_field_name]
            for key, value in field_default.items():
                if key not in json_field:
                    if callable(value):
                        value = safe_call(value, self)
                    json_field[key] = json_deepcopy_with_callable(value)
                    _schema_changed = True
                    self.__dict__["status"] = Status.DIRTY
                    self._mjson_revert_patch.update(
                        {"status": json_encode(old_status), json_field_name: {"_schema_changed": _schema_changed}}
                    )
            for k in [k for k in json_field.keys() if k not in field_default]:
                del json_field[k]
                _schema_changed = True
                self.__dict__["status"] = Status.DIRTY
                self._mjson_revert_patch.update(
                    {"status": json_encode(old_status), json_field_name: {"_schema_changed": _schema_changed}}
                )
        if _schema_changed and settings.IS_UNIT_TEST:
            self._mjson_on_init = deepcopy(self.mjson)
            self._mjson_on_init["status"] = json_encode(old_status)

    def patch(self, **kwargs):
        for subfield_name, value in kwargs.items():
            setattr(self, subfield_name, value)

    def assert_changeable(self, field_name=None):
        super().assert_changeable(field_name)
        if field_name:
            assert field_name not in ("id", "type"), "id, type field 는 절대 수정할 수 없습니다."
            if field_name == "raw":
                assert self.status in (Status.CREATING, Status.NEW), "raw 컬럼은 최초 생성시에만 세팅 가능합니다."
            elif settings.IS_UNIT_TEST:
                assert field_name == "data" or self._force_changer_count >= 1, console_log(
                    "data field 를 제외하면 기본적으로 수정 금지입니다 : {}".format(field_name)
                )
            else:
                # for django admin
                assert (
                    field_name in ("data", "computed") or self._force_changer_count >= 1
                ), "data field 를 제외하면 기본적으로 수정 금지입니다. computed field 는 수정되지 않고 무시됩니다."

    def __setattr__(self, field_name, value):
        # __dict__ setter
        if field_name[0] == "_" or not self._is_initialized:
            self.__dict__[field_name] = value
            return
        # object setter
        # TODO : 모두 확정된 후 로컬상수변수로 대체하여 튜닝
        if field_name in self.field_names_needs_object_setter:
            return object.__setattr__(self, field_name, value)
        # for overrides super().delete()
        if field_name == "id" and value is None:
            return
        # set field
        old = getattr(self, field_name)
        if old != value:
            # for django admin
            if value is None and field_name in ("data", "computed"):
                return
            # check pre condition
            self.assert_changeable(field_name)
            # change value
            if field_name == "data":
                for subfield, subvalue in value.items():
                    setattr(self, subfield, subvalue)
            elif field_name == "status":
                assert old.check_route(value), "해당 status 변경은 허용되지 않습니다: {} --> {}".format(old, value)
                self.__dict__["status"] = value
            elif field_name == "computed":
                assert not settings.IS_UNIT_TEST, "computed field 직접 세팅은 허용되지 않습니다."
                # for django admin
                console_log("try change of computed field is ignored")
            else:
                if self.status in (Status.NORMAL, Status.WORKING):
                    self.__setattr__("status", Status.DIRTY)
                self.__dict__[field_name] = value
            # make revert patch
            if field_name not in self._mjson_revert_patch and field_name in self.field_names:
                self._mjson_revert_patch[field_name] = json_encode(old)
            # raw
            if field_name == "raw":
                assert self.status in (Status.CREATING, Status.NEW)
                self.init_data_by_raw()

    # for cached queryset filter
    def __getattr__(self, key):
        # dot operator 처리 순서 상 이미 __dict__ 는 check 가 된 상태임
        if key[0] == "_":
            raise AttributeError(key)

        parts = key.split("__")
        field_name = parts[0]

        # TODO : ComplexSubfield 에 대해 2-depth 까지만 허용되고 있는데, 이를 확장
        # json field
        if field_name in ("data", "computed", "raw"):
            assert len(parts) in (2, 3)
            subfield_name = parts[1]
            assert getattr(self.__class__, subfield_name).field_name == field_name
            result = getattr(self, subfield_name)
            if len(parts) == 3:
                result = result[parts[2]]
            return result
        # subfield
        elif field_name in self.subfield_names:
            assert len(parts) in (2,)
            subfield_name = parts[0]
            result = getattr(self, subfield_name)[parts[1]]
            return result
        else:
            raise AttributeError("{}.{}".format(self.__class__.__name__, key))

    def get_modified_field_names(self):
        return [field_name for field_name, value in self._mjson_revert_patch.items() if value != {}]

    def create_history(self):
        column_names = [f for f in self.column_names if not f.startswith("computed") and not f.startswith("raw")]
        field_names_str = ",".join(column_names)
        tran = TransactionManager.get_transaction()
        query = "insert into base_modelhistory ({},{}) select {},{} from base_model where id=%s".format(
            "history_transaction", field_names_str, tran.id, field_names_str
        )
        instance_id = self.id
        run_sql(query, params=(instance_id,))

    def on_nosave(self):
        if settings.IS_UNIT_TEST:
            assert self.mjson == self._mjson_on_init, "subfield 를 통하지 않는 등 잘못된 방식으로 필드가 수정되었습니다."
        super().on_nosave()

    def _syncdb_update(self, tran, update_fields=None):
        assert update_fields is None
        update_fields = self.get_modified_field_names()
        assert len(update_fields) >= 2, "수정된 필드가 없는데 syncdb_update() 가 호출되었습니다."
        assert "status" in update_fields and "status" in self._mjson_revert_patch, "status 필드가 적절한 방식으로 수정되지 않았습니다."
        assert (
            "version" not in update_fields and "last_transaction" not in update_fields
        ), "system 필드가 적절한 방식으로 수정되지 않았습니다."
        with ForceChanger(self):
            self.version += 1
            self.last_transaction = tran.id
        if settings.IS_UNIT_TEST:
            mjson_reverted = patch_mjson(deepcopy(self.mjson), MappingProxyType(self._mjson_revert_patch))
            # TODO : schema 만 바뀐 경우에 save() 가 가능하도록 하는 구현 부분 리팩토링
            if "_schema_changed" in mjson_reverted["data"]:
                del mjson_reverted["data"]["_schema_changed"]
            if "_schema_changed" in mjson_reverted["computed"]:
                del mjson_reverted["computed"]["_schema_changed"]
            assert mjson_reverted == self._mjson_on_init, "subfield 를 통하지 않는 등 잘못된 방식으로 필드가 수정되었습니다."
        self.create_history()
        self.computed["_mjson_revert_patch"] = self._mjson_revert_patch
        update_fields.extend(["version", "last_transaction", "computed"])
        super()._syncdb_update(tran, update_fields=update_fields)

    def _mark_delete(self):
        with ForceChanger(self):
            self.computed_uri_hash = None
        super()._mark_delete()

    def on_delete(self):
        subfields = self.subfields
        for rf in subfields["sources"]:
            setattr(self, rf, None)
        for rf in subfields["targets"]:
            setattr(self, rf, None)

    def _syncdb_delete(self, tran):
        self.create_history()
        super()._syncdb_delete(tran)

    def _destroy(self, using=None, keep_parents=True):
        self.create_history()
        super()._destroy(using=None, keep_parents=True)

    def _process_expire_onchange(self, subfield_name=None):
        if subfield_name is None:
            for k, v in self.subfields["data"].items():
                if v.expire == Expire.ON_CHANGE:
                    v.__set__(self, v.default(self))
        else:
            subfields = self.subfields
            if subfield_name in subfields["_dependents"]:
                depended = subfields["_dependents"][subfield_name]
                for v in depended:
                    if v.expire == Expire.ON_CHANGE:
                        v.__set__(self, safe_call(v.default, self))

    def _process_dependent_computed(self, subfield_name):
        subfields = self.subfields
        if subfield_name in subfields["_dependents"]:
            for v in subfields["_dependents"][subfield_name]:
                if v.field_name == "computed":
                    self.compute(v.subfield_name)

    @set_invalid_on_exception
    def onchange_subfield(self, field_name, subfield_name, old, new):
        assert old != new, "old 와 new 값이 같은 경우는 원천적으로 발생되지 않도록 해야 합니다."
        old_status, _mjson_revert_patch, subfields = (self.status, self._mjson_revert_patch, self.subfields)
        subfield = subfields[field_name][subfield_name]
        assert not isinstance(subfield, SubfieldWrapper)
        if old_status == Status.CREATING:
            # TODO : save() 호출 자체를 없애고 나면 이것도 없애기
            # 이때는 on_create 이후 별도로 onchange 이벤트를 일괄 발생시킴
            return
        if subfield_name not in _mjson_revert_patch[field_name]:
            _mjson_revert_patch[field_name][subfield_name] = json_encode(old)
        with ForceChanger(self):
            if old_status not in (Status.NEW, Status.NO_SYNC):
                self.status = Status.DIRTY
            self._process_dependent_computed(subfield_name)
            # original
            onchange_func_name = ONCHANGE_FUNC_NAME.format(subfield_name)
            func = getattr(self, onchange_func_name, None)
            func and func(old, new)
            # wrappers
            for wrapper in subfields["_wrapper_reverse"].get(subfield_name, []):
                onchange_func_name = ONCHANGE_FUNC_NAME.format(wrapper.wrapper_subfield_name)
                func = getattr(self, onchange_func_name, None)
                func and func(old, new)
        self._process_expire_onchange(subfield_name)

    def compute(self, subfield_name):
        # set to computed json
        old_status = self.status
        compute_func = getattr(self, COMPUTE_FUNC_NAME.format(subfield_name))
        value = safe_call(compute_func)
        setattr(self, subfield_name, value)
        # set to computed_field
        computed_field_name = COMPUTED_FIELD_NAME.format(subfield_name)
        if computed_field_name in self.field_names:
            setattr(self, computed_field_name, value)
        if old_status in (Status.NORMAL, Status.WORKING) and self.status == Status.DIRTY:
            self.save()

    def _compute_total(self):
        for subfield_name in self.computed:
            if subfield_name[0] != "_":
                self.compute(subfield_name)

    def compute_uri(self):
        return self.convert_uri(self.uname)

    def compute_all(self):
        for subfield_name in self.subfields["computed"]:
            self.compute(subfield_name)

    @classmethod
    def convert_uri(cls, uname):
        if not uname:
            return None
        uri = cls.uri_format.format(uname if uname[0] != "/" else uname[1:])
        return uri if uri[-2] != "/" else uri[:-1]

    def onchange_uri(self, old, new):
        tran = TransactionManager.get_transaction()
        assert not old or old in tran.uri_mapping, "Transaction.uri_mapping 에 기존 uri 값이 존재하지 않습니다."
        if new in tran.uri_mapping:
            if self.status == Status.NEW:
                tran.remove(self)
            raise DuplicateUriException
        self._old_uri = old
        tran.set(self)

    def compute_uri_hash(self):
        return compute_hash_uuid(self.uri)

    def init_data_by_raw(self):
        pass